tool.xpcool.com/src/views/base64/Base64View.vue

245 lines
7.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">Base64 编解码</h2>
<p class="page-desc">文本 Base64 编码/解码图片文件与 Base64 字符串互转全部在浏览器本地完成</p>
<t-tabs v-model="tab">
<!-- 文本编解码 -->
<t-tab-panel value="text" label="文本编解码">
<t-card class="tool-section" title="输入">
<t-textarea
v-model="textInput"
placeholder="输入要编码/解码的文本"
:autosize="{ minRows: 5, maxRows: 10 }"
/>
<div class="form-actions">
<t-button theme="primary" @click="encodeText">编码(文本 → Base64</t-button>
<t-button theme="primary" variant="outline" @click="decodeText">解码Base64 → 文本)</t-button>
<t-button variant="outline" @click="clearText">清空</t-button>
</div>
</t-card>
<ErrorAlert :message="error" @close="error = ''" />
<t-card v-if="textOutput" class="tool-section" title="结果">
<t-textarea v-model="textOutput" :autosize="{ minRows: 5, maxRows: 10 }" readonly />
<div class="form-actions">
<CopyButton :text="textOutput" />
</div>
</t-card>
</t-tab-panel>
<!-- 图片互转 -->
<t-tab-panel value="image" label="图片互转">
<!-- 图片 -> Base64 -->
<t-card class="tool-section" title="图片 → Base64">
<UploadDrop v-model:files="files" accept="image/*" tip="上传图片,自动生成 Base64 字符串" />
</t-card>
<ErrorAlert :message="imgError" @close="imgError = ''" />
<t-card v-if="imgBase64" class="tool-section" title="Base64 结果">
<t-textarea v-model="imgBase64" :autosize="{ minRows: 4, maxRows: 8 }" readonly />
<div class="form-actions">
<CopyButton :text="imgBase64" />
</div>
</t-card>
<!-- Base64 -> 图片 -->
<t-card class="tool-section" title="Base64 → 图片">
<t-textarea
v-model="b64Input"
placeholder="粘贴图片的 Base64 字符串(可包含 data:image/...;base64, 前缀)"
:autosize="{ minRows: 4, maxRows: 8 }"
/>
<div class="form-actions">
<t-button theme="primary" @click="b64ToImage">转换为图片</t-button>
<t-button variant="outline" @click="clearImage">清空</t-button>
</div>
</t-card>
<ErrorAlert :message="b64Error" @close="b64Error = ''" />
<t-card v-if="imgUrl" class="tool-section" title="转换结果">
<img
:src="imgUrl"
alt="Base64 还原的图片"
class="result-img img-previewable"
title="点击预览大图"
@click="openViewer([imgUrl])"
/>
<div class="form-actions">
<DownloadButton :filename="imgFilename" :get-blob="getImgBlob" label="下载图片" />
</div>
</t-card>
<!-- 结果图放大预览 -->
<AppImageViewer v-model:visible="viewerVisible" v-model:index="viewerIndex" :images="viewerImages" />
</t-tab-panel>
</t-tabs>
<HistoryPanel
:items="history.items"
@select="onHistorySelect"
@remove="history.remove"
@clear="history.clear"
/>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import UploadDrop from '@/components/common/UploadDrop.vue'
import ErrorAlert from '@/components/common/ErrorAlert.vue'
import CopyButton from '@/components/common/CopyButton.vue'
import DownloadButton from '@/components/common/DownloadButton.vue'
import HistoryPanel from '@/components/common/HistoryPanel.vue'
import { useHistory } from '@/composables/useHistory'
import { useImageViewer } from '@/composables/useImageViewer'
import AppImageViewer from '@/components/common/AppImageViewer.vue'
import { dataUrlToBlob, fileToDataUrl } from '@/utils'
const tab = ref('text')
/* ---------- 文本编解码 ---------- */
const textInput = ref('')
const textOutput = ref('')
const error = ref('')
const history = useHistory('base64')
/** 结果图放大预览TDesign ImageViewer */
const { visible: viewerVisible, index: viewerIndex, images: viewerImages, open: openViewer } = useImageViewer()
/** 编码UTF-8 安全的 base64先转字节再 btoa避免中文报错 */
function encodeText() {
error.value = ''
textOutput.value = ''
if (!textInput.value) {
error.value = '请输入要编码的文本'
return
}
try {
const bytes = new TextEncoder().encode(textInput.value)
let bin = ''
bytes.forEach((b) => (bin += String.fromCharCode(b)))
textOutput.value = btoa(bin)
history.add(textInput.value)
} catch {
error.value = '编码失败:输入包含无法处理的字符'
}
}
function decodeText() {
error.value = ''
textOutput.value = ''
if (!textInput.value.trim()) {
error.value = '请输入要解码的 Base64 文本'
return
}
try {
const bin = atob(textInput.value.trim())
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0))
textOutput.value = new TextDecoder('utf-8', { fatal: false }).decode(bytes)
} catch {
error.value = '解码失败:不是合法的 Base64 字符串'
}
}
function clearText() {
textInput.value = ''
textOutput.value = ''
error.value = ''
}
/* ---------- 图片互转 ---------- */
const files = ref<File[]>([])
const imgBase64 = ref('')
const imgError = ref('')
const b64Input = ref('')
const b64Error = ref('')
const imgUrl = ref('')
const imgMime = ref('image/png')
const imgFilename = computed(() => {
const ext = imgMime.value.split('/')[1] ?? 'png'
return `decoded_image.${ext}`
})
/** 文件变化时(含拖拽删除/清空)自动转 Base64 */
watch(files, (list) => {
const raw = list[0]
imgError.value = ''
imgBase64.value = ''
if (!(raw instanceof File)) return
void convertToBase64(raw)
})
async function convertToBase64(raw: File) {
try {
imgBase64.value = await fileToDataUrl(raw)
} catch {
imgError.value = '读取图片失败,请重试'
}
}
/** Base64 -> 图片:校验格式后渲染预览并生成 Blob */
function b64ToImage() {
b64Error.value = ''
imgUrl.value = ''
const raw = b64Input.value.trim()
if (!raw) {
b64Error.value = '请粘贴图片的 Base64 字符串'
return
}
try {
// 兼容带 data:image 前缀与纯 Base64 两种输入
const dataUrl = raw.startsWith('data:') ? raw : `data:image/png;base64,${raw}`
const blob = dataUrlToBlob(dataUrl)
if (!blob.type.startsWith('image/')) throw new Error('not image')
imgMime.value = blob.type
imgUrl.value = dataUrl
} catch {
b64Error.value = '转换失败Base64 数据不合法或不是图片'
}
}
function getImgBlob(): Blob | null {
if (!imgUrl.value) return null
try {
return dataUrlToBlob(imgUrl.value)
} catch {
return null
}
}
function clearImage() {
b64Input.value = ''
b64Error.value = ''
imgUrl.value = ''
}
function onHistorySelect(text: string) {
tab.value = 'text'
textInput.value = text
encodeText()
}
</script>
<style scoped>
.form-actions {
display: flex;
gap: 8px;
margin-top: 16px;
flex-wrap: wrap;
}
.result-img {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
border: 1px solid var(--td-border-level-1-color);
margin-bottom: 12px;
}
</style>