Compare commits
4 Commits
c6d41d18b3
...
feature/i1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e397d03850 | ||
|
|
ada0dfa3cc | ||
|
|
2a07fd950f | ||
| 8e5eea02f1 |
66
deploy.sh
Executable file
66
deploy.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 部署脚本:打包并通过 SSH 将构建产物同步到远程目录
|
||||||
|
# 用法: ./deploy.sh <服务器IP> <SSH端口> <登录账户> <登录密码> [sudo]
|
||||||
|
# 示例: ./deploy.sh 192.168.1.100 22 root mypassword
|
||||||
|
# 若远程目录无写权限,加第5个参数 sudo:先传到 /tmp,再通过 sudo 拷到目标(会用到同一密码作为 sudo 密码)
|
||||||
|
# 示例: ./deploy.sh 192.168.1.100 22 myuser mypassword sudo
|
||||||
|
|
||||||
|
if [ $# -lt 4 ]; then
|
||||||
|
echo "用法: $0 <服务器IP> <SSH端口> <登录账户> <登录密码> [sudo]"
|
||||||
|
echo "示例: $0 192.168.1.100 22 root mypassword"
|
||||||
|
echo "远程目录无写权限时加第5参数: $0 <IP> <端口> <用户> <密码> sudo"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HOST="$1"
|
||||||
|
PORT="$2"
|
||||||
|
USER="$3"
|
||||||
|
PASSWORD="$4"
|
||||||
|
USE_SUDO="${5:-}"
|
||||||
|
REMOTE_DIR="/root/caddy/site/tool"
|
||||||
|
REMOTE_TMP="/tmp/tool_deploy_$$"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DIST_DIR="${SCRIPT_DIR}/dist"
|
||||||
|
SSH_OPTS="-p ${PORT} -o StrictHostKeyChecking=accept-new -T"
|
||||||
|
|
||||||
|
echo ">>> 正在打包..."
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
if [ ! -d "$DIST_DIR" ]; then
|
||||||
|
echo "错误: 构建产物目录 dist 不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查 sshpass 是否可用(用于非交互式传入密码)
|
||||||
|
if ! command -v sshpass &> /dev/null; then
|
||||||
|
echo "未找到 sshpass,请先安装:"
|
||||||
|
echo " macOS: brew install sshpass"
|
||||||
|
echo " Ubuntu: sudo apt-get install sshpass"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export SSHPASS="$PASSWORD"
|
||||||
|
|
||||||
|
if [ "$USE_SUDO" = "sudo" ]; then
|
||||||
|
echo ">>> 正在同步到远程临时目录 ${REMOTE_TMP}"
|
||||||
|
sshpass -e rsync -avz --delete \
|
||||||
|
-e "ssh ${SSH_OPTS}" \
|
||||||
|
"${DIST_DIR}/" \
|
||||||
|
"${USER}@${HOST}:${REMOTE_TMP}/"
|
||||||
|
echo ">>> 正在用 sudo 拷贝到目标目录 ${REMOTE_DIR}"
|
||||||
|
# 只执行一次 sudo(读一次密码),在子 shell 里完成 rsync 与清理,避免第二次 sudo 无密码
|
||||||
|
printf '%s\n' "$PASSWORD" | sshpass -e ssh ${SSH_OPTS} "${USER}@${HOST}" \
|
||||||
|
"sudo -S sh -c 'rsync -avz --delete ${REMOTE_TMP}/ ${REMOTE_DIR}/ && rm -rf ${REMOTE_TMP}'"
|
||||||
|
else
|
||||||
|
echo ">>> 正在通过 SSH 同步到 ${USER}@${HOST}:${PORT} -> ${REMOTE_DIR}"
|
||||||
|
sshpass -e rsync -avz --delete \
|
||||||
|
-e "ssh ${SSH_OPTS}" \
|
||||||
|
"${DIST_DIR}/" \
|
||||||
|
"${USER}@${HOST}:${REMOTE_DIR}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset SSHPASS
|
||||||
|
echo ">>> 部署完成"
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>RC707的工具箱</title>
|
<title>RC707的工具箱</title>
|
||||||
<!-- Google tag (gtag.js) -->
|
<!-- Google tag (gtag.js) -->
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C2H4BGZJBD"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C2H4BGZJBD"></script>
|
||||||
<script>
|
<script>
|
||||||
window.dataLayer = window.dataLayer || [];
|
window.dataLayer = window.dataLayer || [];
|
||||||
function gtag(){dataLayer.push(arguments);}
|
function gtag(){dataLayer.push(arguments);}
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "toolbox",
|
"name": "toolbox",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A Vue-based toolbox application",
|
"description": "A Vue-based toolbox application",
|
||||||
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -10,8 +11,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^7.1.0",
|
"@fortawesome/fontawesome-free": "^7.1.0",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
|
"vue-i18n": "^9.14.4",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
212
src/App.vue
212
src/App.vue
@@ -2,20 +2,55 @@
|
|||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<nav class="navbar">
|
<nav class="navbar">
|
||||||
<div class="nav-content">
|
<div class="nav-content">
|
||||||
<router-link to="/" class="logo">
|
<router-link :to="localePath(currentPathLocale, '')" class="logo">
|
||||||
<h1>RC707的工具箱</h1>
|
<h1>{{ t('app.title') }}</h1>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="nav-links">
|
<div class="nav-right">
|
||||||
<router-link to="/" class="nav-link">首页</router-link>
|
<div class="nav-links">
|
||||||
<router-link to="/json-formatter" class="nav-link">JSON</router-link>
|
<router-link :to="localePath(currentPathLocale, '')" class="nav-link">{{ t('nav.home') }}</router-link>
|
||||||
<router-link to="/comparator" class="nav-link">对比</router-link>
|
<router-link :to="localePath(currentPathLocale, 'json-formatter')" class="nav-link">{{ t('nav.json') }}</router-link>
|
||||||
<router-link to="/encoder-decoder" class="nav-link">编解码</router-link>
|
<router-link :to="localePath(currentPathLocale, 'comparator')" class="nav-link">{{ t('nav.comparator') }}</router-link>
|
||||||
<router-link to="/variable-name" class="nav-link">变量名</router-link>
|
<router-link :to="localePath(currentPathLocale, 'encoder-decoder')" class="nav-link">{{ t('nav.encoderDecoder') }}</router-link>
|
||||||
<router-link to="/qr-code" class="nav-link">二维码</router-link>
|
<router-link :to="localePath(currentPathLocale, 'variable-name')" class="nav-link">{{ t('nav.variableName') }}</router-link>
|
||||||
<router-link to="/timestamp-converter" class="nav-link">时间戳</router-link>
|
<router-link :to="localePath(currentPathLocale, 'qr-code')" class="nav-link">{{ t('nav.qrCode') }}</router-link>
|
||||||
<router-link to="/color-converter" class="nav-link">颜色</router-link>
|
<router-link :to="localePath(currentPathLocale, 'timestamp-converter')" class="nav-link">{{ t('nav.timestamp') }}</router-link>
|
||||||
|
<router-link :to="localePath(currentPathLocale, 'color-converter')" class="nav-link">{{ t('nav.color') }}</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div ref="localeDropRef" class="nav-locale">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="locale-trigger"
|
||||||
|
:aria-expanded="localeOpen"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-label="选择语言"
|
||||||
|
@click="localeOpen = !localeOpen"
|
||||||
|
>
|
||||||
|
{{ currentLocaleLabel }}
|
||||||
|
</button>
|
||||||
|
<Transition name="locale-drop">
|
||||||
|
<div
|
||||||
|
v-show="localeOpen"
|
||||||
|
class="locale-dropdown"
|
||||||
|
role="listbox"
|
||||||
|
aria-label="语言选项"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="opt in localeOptions"
|
||||||
|
:key="opt.pathLocale"
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
:aria-selected="currentPathLocale === opt.pathLocale"
|
||||||
|
class="locale-option"
|
||||||
|
:class="{ active: currentPathLocale === opt.pathLocale }"
|
||||||
|
@click="selectLocale(opt.pathLocale)"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<router-view />
|
<router-view />
|
||||||
@@ -24,7 +59,55 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// Vue 3 Composition API
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { localePath } from './router'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const localeOpen = ref(false)
|
||||||
|
const localeDropRef = ref(null)
|
||||||
|
|
||||||
|
const currentPathLocale = computed(() => route.params.locale || 'zh')
|
||||||
|
|
||||||
|
const localeOptions = [
|
||||||
|
{ pathLocale: 'zh', label: '简体中文' },
|
||||||
|
{ pathLocale: 'zh-tw', label: '繁體中文' },
|
||||||
|
{ pathLocale: 'en', label: 'English' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentLocaleLabel = computed(() => {
|
||||||
|
const opt = localeOptions.find(o => o.pathLocale === currentPathLocale.value)
|
||||||
|
return opt ? opt.label : '简体中文'
|
||||||
|
})
|
||||||
|
|
||||||
|
function switchLocale(newPathLocale) {
|
||||||
|
if (newPathLocale === currentPathLocale.value) return
|
||||||
|
const pathWithoutLocale = route.path.replace(/^\/[^/]+/, '') || ''
|
||||||
|
const segment = pathWithoutLocale.startsWith('/') ? pathWithoutLocale.slice(1) : pathWithoutLocale
|
||||||
|
router.push(localePath(newPathLocale, segment))
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLocale(pathLocale) {
|
||||||
|
switchLocale(pathLocale)
|
||||||
|
localeOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickOutside(e) {
|
||||||
|
if (localeDropRef.value && !localeDropRef.value.contains(e.target)) {
|
||||||
|
localeOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', onClickOutside)
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', onClickOutside)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -42,6 +125,9 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-content {
|
.nav-content {
|
||||||
@@ -52,6 +138,8 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@@ -68,6 +156,12 @@
|
|||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-links {
|
.nav-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
@@ -97,12 +191,84 @@
|
|||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link.router-link-active {
|
.nav-link.router-link-exact-active {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
border-bottom-color: #1a1a1a;
|
border-bottom-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-locale {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-trigger {
|
||||||
|
min-width: 5.5rem;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-trigger:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
right: 0;
|
||||||
|
min-width: 8.5rem;
|
||||||
|
padding: 4px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-option {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-option:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-option.active {
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-drop-enter-active,
|
||||||
|
.locale-drop-leave-active {
|
||||||
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locale-drop-enter-from,
|
||||||
|
.locale-drop-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -125,20 +291,36 @@
|
|||||||
font-size: 1.125rem;
|
font-size: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links {
|
.navbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-content {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-right {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-locale {
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
6
src/AppRoot.vue
Normal file
6
src/AppRoot.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
@@ -9,14 +9,14 @@
|
|||||||
<!-- 年月导航 -->
|
<!-- 年月导航 -->
|
||||||
<div class="date-header">
|
<div class="date-header">
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<button @click="prevYear" class="nav-btn" title="上一年">«</button>
|
<button @click="prevYear" class="nav-btn" :title="t('dateTimePicker.prevYear')">«</button>
|
||||||
<button @click="prevMonth" class="nav-btn" title="上一月">‹</button>
|
<button @click="prevMonth" class="nav-btn" :title="t('dateTimePicker.prevMonth')">‹</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!isEditingMonthYear"
|
v-if="!isEditingMonthYear"
|
||||||
@click="startEditingMonthYear"
|
@click="startEditingMonthYear"
|
||||||
class="current-month-year editable"
|
class="current-month-year editable"
|
||||||
title="点击输入年月"
|
:title="t('dateTimePicker.clickInputMonthYear')"
|
||||||
>
|
>
|
||||||
{{ currentViewDate.getFullYear() }} - {{ String(currentViewDate.getMonth() + 1).padStart(2, '0') }}
|
{{ currentViewDate.getFullYear() }} - {{ String(currentViewDate.getMonth() + 1).padStart(2, '0') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -27,18 +27,18 @@
|
|||||||
@keyup.enter="confirmMonthYear"
|
@keyup.enter="confirmMonthYear"
|
||||||
@keyup.esc="cancelEditingMonthYear"
|
@keyup.esc="cancelEditingMonthYear"
|
||||||
class="month-year-input"
|
class="month-year-input"
|
||||||
placeholder="YYYY-MM"
|
:placeholder="t('dateTimePicker.placeholderMonthYear')"
|
||||||
ref="monthYearInputRef"
|
ref="monthYearInputRef"
|
||||||
/>
|
/>
|
||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<button @click="nextMonth" class="nav-btn" title="下一月">›</button>
|
<button @click="nextMonth" class="nav-btn" :title="t('dateTimePicker.nextMonth')">›</button>
|
||||||
<button @click="nextYear" class="nav-btn" title="下一年">»</button>
|
<button @click="nextYear" class="nav-btn" :title="t('dateTimePicker.nextYear')">»</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 星期标题 -->
|
<!-- 星期标题 -->
|
||||||
<div class="weekdays">
|
<div class="weekdays">
|
||||||
<div class="weekday" v-for="day in ['日', '一', '二', '三', '四', '五', '六']" :key="day">
|
<div class="weekday" v-for="(day, idx) in weekdays" :key="idx">
|
||||||
{{ day }}
|
{{ day }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,12 +62,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 此刻按钮 -->
|
<!-- 此刻按钮 -->
|
||||||
<button @click="selectNow" class="now-btn">此刻</button>
|
<button @click="selectNow" class="now-btn">{{ t('dateTimePicker.now') }}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧时间选择区域 -->
|
<!-- 右侧时间选择区域 -->
|
||||||
<div class="time-picker-section">
|
<div class="time-picker-section">
|
||||||
<div class="time-header">选择时间</div>
|
<div class="time-header">{{ t('dateTimePicker.selectTime') }}</div>
|
||||||
|
|
||||||
<div class="time-selectors">
|
<div class="time-selectors">
|
||||||
<!-- 小时选择 -->
|
<!-- 小时选择 -->
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 确定按钮 -->
|
<!-- 确定按钮 -->
|
||||||
<button @click="confirmSelection" class="confirm-btn">确定</button>
|
<button @click="confirmSelection" class="confirm-btn">{{ t('dateTimePicker.confirm') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,6 +132,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t, tm } = useI18n()
|
||||||
|
|
||||||
|
const weekdays = computed(() => {
|
||||||
|
const msg = tm('dateTimePicker')
|
||||||
|
const w = msg?.weekdays
|
||||||
|
return Array.isArray(w) ? w : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
|
|||||||
17
src/i18n/index.js
Normal file
17
src/i18n/index.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import zhCN from './locales/zh-CN'
|
||||||
|
import zhTW from './locales/zh-TW'
|
||||||
|
import en from './locales/en'
|
||||||
|
|
||||||
|
export const supportedLocales = ['zh-CN', 'zh-TW', 'en']
|
||||||
|
|
||||||
|
export const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'zh-CN',
|
||||||
|
fallbackLocale: 'zh-CN',
|
||||||
|
messages: {
|
||||||
|
'zh-CN': zhCN,
|
||||||
|
'zh-TW': zhTW,
|
||||||
|
en,
|
||||||
|
},
|
||||||
|
})
|
||||||
293
src/i18n/locales/en.js
Normal file
293
src/i18n/locales/en.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
export default {
|
||||||
|
nav: {
|
||||||
|
home: 'Home',
|
||||||
|
json: 'JSON',
|
||||||
|
comparator: 'Compare',
|
||||||
|
encoderDecoder: 'Encode/Decode',
|
||||||
|
variableName: 'Variable',
|
||||||
|
qrCode: 'QR Code',
|
||||||
|
timestamp: 'Timestamp',
|
||||||
|
color: 'Color',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
title: "RC707's Toolbox",
|
||||||
|
titleJson: "RC707's Toolbox - JSON",
|
||||||
|
titleComparator: "RC707's Toolbox - Compare",
|
||||||
|
titleEncoderDecoder: "RC707's Toolbox - Encode/Decode",
|
||||||
|
titleVariableName: "RC707's Toolbox - Variable",
|
||||||
|
titleQrCode: "RC707's Toolbox - QR Code",
|
||||||
|
titleTimestamp: "RC707's Toolbox - Timestamp",
|
||||||
|
titleColor: "RC707's Toolbox - Color",
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
heroToday: 'Today is {date}',
|
||||||
|
toolJson: 'JSON',
|
||||||
|
toolJsonDesc: 'Format, validate and beautify JSON',
|
||||||
|
toolComparator: 'Compare',
|
||||||
|
toolComparatorDesc: 'Text and JSON comparison',
|
||||||
|
toolEncoderDecoder: 'Encode/Decode',
|
||||||
|
toolEncoderDecoderDesc: 'Encoding/decoding tools',
|
||||||
|
toolVariableName: 'Variable',
|
||||||
|
toolVariableNameDesc: 'Variable name format conversion',
|
||||||
|
toolQrCode: 'QR Code',
|
||||||
|
toolQrCodeDesc: 'Generate QR codes',
|
||||||
|
toolTimestamp: 'Timestamp',
|
||||||
|
toolTimestampDesc: 'Timestamp and date string conversion',
|
||||||
|
toolColor: 'Color',
|
||||||
|
toolColorDesc: 'Color format conversion',
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
zhCN: '简',
|
||||||
|
zhTW: '繁',
|
||||||
|
en: 'EN',
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
close: 'Close',
|
||||||
|
copy: 'Copy',
|
||||||
|
paste: 'Paste',
|
||||||
|
clear: 'Clear',
|
||||||
|
history: 'History',
|
||||||
|
noHistory: 'No history yet',
|
||||||
|
copied: 'Copied to clipboard',
|
||||||
|
copyFailed: 'Copy failed: ',
|
||||||
|
clipboardEmpty: 'Clipboard is empty',
|
||||||
|
pasteHint: 'Press Ctrl+V or Cmd+V to paste',
|
||||||
|
pasteFailed: 'Cannot access editor, please paste manually',
|
||||||
|
cleared: 'Cleared',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
editor: 'Editor',
|
||||||
|
maxSize: '(max 5MB)',
|
||||||
|
tree: 'Tree',
|
||||||
|
placeholder: 'Enter or paste JSON, e.g. {"name":"toolbox","version":1.0}',
|
||||||
|
jsonPathPlaceholder: 'Enter JSONPath, e.g. $.key.subkey',
|
||||||
|
jsonPathFilter: 'JSONPath filter',
|
||||||
|
clearFilter: 'Clear filter',
|
||||||
|
copyFilterResult: 'Copy filter result',
|
||||||
|
expandAll: 'Expand all',
|
||||||
|
collapseAll: 'Collapse all',
|
||||||
|
format: 'Format',
|
||||||
|
minify: 'Minify',
|
||||||
|
escape: 'Escape',
|
||||||
|
unescape: 'Unescape',
|
||||||
|
emptyState: 'Enter or paste JSON on the left, tree view will show on the right',
|
||||||
|
noMatchedNodes: 'No matching nodes',
|
||||||
|
noContentToCopy: 'Nothing to copy',
|
||||||
|
copiedCount: 'Copied {count} match(es)',
|
||||||
|
contentOverLimit: 'Content exceeds 5MB limit, truncated',
|
||||||
|
pleaseInputJson: 'Please enter JSON',
|
||||||
|
inputOverLimit: 'Input exceeds 5MB limit, cannot format',
|
||||||
|
outputOverLimit: 'Formatted output exceeds 5MB limit',
|
||||||
|
formatSuccess: 'Formatted',
|
||||||
|
jsonError: 'JSON error: ',
|
||||||
|
minifyOverLimit: 'Input exceeds 5MB limit, cannot minify',
|
||||||
|
minifyOutputOverLimit: 'Minified output exceeds 5MB limit',
|
||||||
|
minifySuccess: 'Minified',
|
||||||
|
escapeOverLimit: 'Input exceeds 5MB limit, cannot escape',
|
||||||
|
escapeOutputOverLimit: 'Escaped output exceeds 5MB limit',
|
||||||
|
escapeSuccess: 'Escaped',
|
||||||
|
escapeFailed: 'Escape failed: ',
|
||||||
|
unescapeOverLimit: 'Input exceeds 5MB limit, cannot unescape',
|
||||||
|
unescapeOutputOverLimit: 'Unescaped output exceeds 5MB limit',
|
||||||
|
unescapeSuccess: 'Unescaped',
|
||||||
|
unescapeFormatSuccess: 'Unescaped and formatted',
|
||||||
|
unescapeFailed: 'Unescape failed: ',
|
||||||
|
editorEmpty: 'Editor is empty, nothing to copy',
|
||||||
|
pasteOverLimit: 'Pasted content exceeds 5MB limit, truncated',
|
||||||
|
},
|
||||||
|
encoder: {
|
||||||
|
input: 'Input',
|
||||||
|
output: 'Output',
|
||||||
|
encode: 'Encode',
|
||||||
|
decode: 'Decode',
|
||||||
|
base64: 'Base64',
|
||||||
|
url: 'URL',
|
||||||
|
unicode: 'Unicode',
|
||||||
|
zlib: 'Zlib',
|
||||||
|
titleBase64: 'Base64 encode',
|
||||||
|
titleUrl: 'URL encode',
|
||||||
|
titleUnicode: 'Unicode encode',
|
||||||
|
titleZlib: 'Zlib compress/decompress',
|
||||||
|
copyOutput: 'Copy output',
|
||||||
|
inputPlaceholder: 'Enter text to encode or decode',
|
||||||
|
outputPlaceholder: 'Encoded or decoded result will appear here',
|
||||||
|
pleaseInputEncode: 'Please enter text to encode',
|
||||||
|
encodeSuccess: 'Encoded',
|
||||||
|
encodeFailed: 'Encode failed: ',
|
||||||
|
pleaseInputDecode: 'Please enter string to decode',
|
||||||
|
decodeSuccess: 'Decoded',
|
||||||
|
decodeFailed: 'Decode failed: invalid {type} string',
|
||||||
|
inputEmptyCopy: 'Input is empty, nothing to copy',
|
||||||
|
copiedInput: 'Input copied to clipboard',
|
||||||
|
outputEmptyCopy: 'Output is empty, nothing to copy',
|
||||||
|
copiedOutput: 'Output copied to clipboard',
|
||||||
|
manualPaste: 'Please paste manually',
|
||||||
|
},
|
||||||
|
variable: {
|
||||||
|
placeholder: 'Enter variable name (any format)',
|
||||||
|
camelCase: 'camelCase',
|
||||||
|
pascalCase: 'PascalCase',
|
||||||
|
snakeCase: 'snake_case',
|
||||||
|
kebabCase: 'kebab-case',
|
||||||
|
constantCase: 'CONSTANT_CASE',
|
||||||
|
copyLabel: 'Copy {label}',
|
||||||
|
empty: '—',
|
||||||
|
noContentToCopy: 'Nothing to copy',
|
||||||
|
copiedLabel: '{label} copied to clipboard',
|
||||||
|
},
|
||||||
|
qr: {
|
||||||
|
inputPlaceholder: 'Enter content for QR code',
|
||||||
|
generate: 'Generate QR code',
|
||||||
|
download: 'Download',
|
||||||
|
copyImage: 'Copy image',
|
||||||
|
qrCode: 'QR code',
|
||||||
|
pleaseInput: 'Please enter content for QR code',
|
||||||
|
generateSuccess: 'QR code generated',
|
||||||
|
generateFailed: 'Failed to generate: ',
|
||||||
|
noQrToDownload: 'No QR code to download',
|
||||||
|
downloadSuccess: 'Downloaded',
|
||||||
|
downloadFailed: 'Download failed: ',
|
||||||
|
noQrToCopy: 'No QR code to copy',
|
||||||
|
copyImageFailed: 'Copy failed, use download instead',
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
dateToTs: 'Date → ({tz}) Timestamp:',
|
||||||
|
tsToDate: 'Timestamp → ({tz}) Date',
|
||||||
|
currentTs: 'Current timestamp:',
|
||||||
|
placeholderTs: 'Enter timestamp',
|
||||||
|
selectDateTime: 'Select date & time',
|
||||||
|
resetData: 'Reset',
|
||||||
|
resume: 'Resume',
|
||||||
|
pause: 'Pause',
|
||||||
|
seconds: 'Seconds',
|
||||||
|
milliseconds: 'Milliseconds',
|
||||||
|
nanoseconds: 'Nanoseconds',
|
||||||
|
datePlaceholderSeconds: 'Format: yyyy-MM-dd HH:mm:ss',
|
||||||
|
datePlaceholderMs: 'Format: yyyy-MM-dd HH:mm:ss.SSS',
|
||||||
|
datePlaceholderNs: 'Format: yyyy-MM-dd HH:mm:ss.SSSSSSSSS',
|
||||||
|
dataReset: 'Data reset',
|
||||||
|
invalidNs: 'Please enter a valid nanosecond timestamp',
|
||||||
|
invalidNumber: 'Please enter a valid number',
|
||||||
|
invalidTs: 'Invalid timestamp',
|
||||||
|
convertFailed: 'Convert failed: ',
|
||||||
|
invalidDateFormat: 'Invalid date format',
|
||||||
|
noContentToCopy: 'Nothing to copy',
|
||||||
|
copyFailed: 'Copy failed',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
rgb: 'RGB',
|
||||||
|
hex: 'Hex',
|
||||||
|
hsl: 'HSL',
|
||||||
|
copyRgb: 'Copy RGB',
|
||||||
|
pasteRgb: 'Paste RGB',
|
||||||
|
copyHex: 'Copy hex',
|
||||||
|
pasteHex: 'Paste hex',
|
||||||
|
copyHsl: 'Copy HSL',
|
||||||
|
pasteHsl: 'Paste HSL',
|
||||||
|
placeholderRgb: '0-255',
|
||||||
|
placeholderHex: 'FFFFFF',
|
||||||
|
placeholderH: '0-360',
|
||||||
|
placeholderSL: '0-100',
|
||||||
|
reset: 'Reset',
|
||||||
|
random: 'Random',
|
||||||
|
rgbPasted: 'RGB pasted',
|
||||||
|
rgbOutOfRange: 'RGB out of range (0-255)',
|
||||||
|
hslPasted: 'HSL pasted',
|
||||||
|
hslOutOfRange: 'HSL out of range (H: 0-360, S/L: 0-100)',
|
||||||
|
rgbCopied: 'RGB copied to clipboard',
|
||||||
|
hexCopied: 'Hex copied to clipboard',
|
||||||
|
hslCopied: 'HSL copied to clipboard',
|
||||||
|
pasteSuccess: 'Pasted',
|
||||||
|
invalidRgb: 'Clipboard is not valid RGB',
|
||||||
|
invalidHex: 'Clipboard is not valid hex',
|
||||||
|
invalidHsl: 'Clipboard is not valid HSL',
|
||||||
|
manualPaste: 'Please paste into input manually',
|
||||||
|
pasteFailed: 'Paste failed',
|
||||||
|
},
|
||||||
|
comparator: {
|
||||||
|
textCompare: 'Text',
|
||||||
|
jsonCompare: 'JSON',
|
||||||
|
lineMode: 'By line',
|
||||||
|
charMode: 'By char',
|
||||||
|
ignoreListOrder: 'Ignore list order',
|
||||||
|
compare: 'Compare',
|
||||||
|
startCompare: 'Compare',
|
||||||
|
textA: 'Text A',
|
||||||
|
textB: 'Text B',
|
||||||
|
maxSize: '(max {size})',
|
||||||
|
placeholderA: 'Enter or paste content A',
|
||||||
|
placeholderB: 'Enter or paste content B',
|
||||||
|
result: 'Result',
|
||||||
|
same: 'Same:',
|
||||||
|
insert: 'Insert:',
|
||||||
|
delete: 'Delete:',
|
||||||
|
modify: 'Modify:',
|
||||||
|
fullscreen: 'Fullscreen',
|
||||||
|
exitFullscreen: 'Exit fullscreen',
|
||||||
|
left: 'Left',
|
||||||
|
right: 'Right',
|
||||||
|
contentOverLimit: 'Content exceeds {size} limit, truncated',
|
||||||
|
emptyCopy: '{side} is empty, nothing to copy',
|
||||||
|
copiedSide: '{side} copied to clipboard',
|
||||||
|
pasteOverLimit: 'Paste exceeds {size} limit, truncated',
|
||||||
|
pleaseInput: 'Please enter content to compare',
|
||||||
|
inputOverLimit: 'Input exceeds {size} limit',
|
||||||
|
largeArrayHint: 'Large array detected, comparison may take a while...',
|
||||||
|
compareDone: 'Done',
|
||||||
|
noResultToCopy: 'No result to copy',
|
||||||
|
resultCopied: 'Result copied to clipboard',
|
||||||
|
},
|
||||||
|
dateTimePicker: {
|
||||||
|
prevYear: 'Prev year',
|
||||||
|
prevMonth: 'Prev month',
|
||||||
|
nextMonth: 'Next month',
|
||||||
|
nextYear: 'Next year',
|
||||||
|
clickInputMonthYear: 'Click to input year-month',
|
||||||
|
placeholderMonthYear: 'YYYY-MM',
|
||||||
|
weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
||||||
|
now: 'Now',
|
||||||
|
selectTime: 'Select time',
|
||||||
|
confirm: 'OK',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
},
|
||||||
|
timestampTz: {
|
||||||
|
'UTC-12:00': 'Baker Island',
|
||||||
|
'UTC-11:00': 'Samoa',
|
||||||
|
'UTC-10:00': 'Hawaii',
|
||||||
|
'UTC-09:30': 'Marquesas',
|
||||||
|
'UTC-09:00': 'Alaska',
|
||||||
|
'UTC-08:00': 'Los Angeles',
|
||||||
|
'UTC-07:00': 'Denver',
|
||||||
|
'UTC-06:00': 'Chicago',
|
||||||
|
'UTC-05:00': 'New York',
|
||||||
|
'UTC-04:00': 'Caracas',
|
||||||
|
'UTC-03:30': 'Newfoundland',
|
||||||
|
'UTC-03:00': 'Buenos Aires',
|
||||||
|
'UTC-02:00': 'Mid-Atlantic',
|
||||||
|
'UTC-01:00': 'Azores',
|
||||||
|
'UTC+00:00': 'London',
|
||||||
|
'UTC+01:00': 'Paris',
|
||||||
|
'UTC+02:00': 'Cairo',
|
||||||
|
'UTC+03:00': 'Moscow',
|
||||||
|
'UTC+03:30': 'Tehran',
|
||||||
|
'UTC+04:00': 'Dubai',
|
||||||
|
'UTC+04:30': 'Kabul',
|
||||||
|
'UTC+05:00': 'Islamabad',
|
||||||
|
'UTC+05:30': 'New Delhi',
|
||||||
|
'UTC+05:45': 'Kathmandu',
|
||||||
|
'UTC+06:00': 'Dhaka',
|
||||||
|
'UTC+06:30': 'Yangon',
|
||||||
|
'UTC+07:00': 'Bangkok',
|
||||||
|
'UTC+08:00': 'Beijing',
|
||||||
|
'UTC+08:45': 'Eucla',
|
||||||
|
'UTC+09:00': 'Tokyo',
|
||||||
|
'UTC+09:30': 'Adelaide',
|
||||||
|
'UTC+10:00': 'Sydney',
|
||||||
|
'UTC+10:30': 'Lord Howe',
|
||||||
|
'UTC+11:00': 'Noumea',
|
||||||
|
'UTC+12:00': 'Auckland',
|
||||||
|
'UTC+12:45': 'Chatham',
|
||||||
|
'UTC+13:00': 'Samoa',
|
||||||
|
'UTC+14:00': 'Kiribati',
|
||||||
|
},
|
||||||
|
}
|
||||||
293
src/i18n/locales/zh-CN.js
Normal file
293
src/i18n/locales/zh-CN.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
export default {
|
||||||
|
nav: {
|
||||||
|
home: '首页',
|
||||||
|
json: 'JSON',
|
||||||
|
comparator: '对比',
|
||||||
|
encoderDecoder: '编解码',
|
||||||
|
variableName: '变量名',
|
||||||
|
qrCode: '二维码',
|
||||||
|
timestamp: '时间戳',
|
||||||
|
color: '颜色',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
title: 'RC707的工具箱',
|
||||||
|
titleJson: 'RC707的工具箱-JSON',
|
||||||
|
titleComparator: 'RC707的工具箱-对比',
|
||||||
|
titleEncoderDecoder: 'RC707的工具箱-编解码',
|
||||||
|
titleVariableName: 'RC707的工具箱-变量名',
|
||||||
|
titleQrCode: 'RC707的工具箱-二维码',
|
||||||
|
titleTimestamp: 'RC707的工具箱-时间戳',
|
||||||
|
titleColor: 'RC707的工具箱-颜色',
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
heroToday: '今天是{date}',
|
||||||
|
toolJson: 'JSON',
|
||||||
|
toolJsonDesc: '格式化、验证和美化JSON数据',
|
||||||
|
toolComparator: '对比',
|
||||||
|
toolComparatorDesc: '文本和JSON对比工具',
|
||||||
|
toolEncoderDecoder: '编解码',
|
||||||
|
toolEncoderDecoderDesc: '编码/解码工具',
|
||||||
|
toolVariableName: '变量名',
|
||||||
|
toolVariableNameDesc: '变量名格式转换',
|
||||||
|
toolQrCode: '二维码',
|
||||||
|
toolQrCodeDesc: '生成二维码',
|
||||||
|
toolTimestamp: '时间戳',
|
||||||
|
toolTimestampDesc: '时间戳与时间字符串相互转换',
|
||||||
|
toolColor: '颜色',
|
||||||
|
toolColorDesc: '颜色格式转换',
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
zhCN: '简体',
|
||||||
|
zhTW: '繁中',
|
||||||
|
en: 'EN',
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
close: '关闭',
|
||||||
|
copy: '复制',
|
||||||
|
paste: '粘贴',
|
||||||
|
clear: '清空',
|
||||||
|
history: '历史记录',
|
||||||
|
noHistory: '暂无历史记录',
|
||||||
|
copied: '已复制到剪贴板',
|
||||||
|
copyFailed: '复制失败:',
|
||||||
|
clipboardEmpty: '剪贴板内容为空',
|
||||||
|
pasteHint: '请按 Ctrl+V 或 Cmd+V 粘贴内容',
|
||||||
|
pasteFailed: '无法访问编辑器,请手动粘贴内容',
|
||||||
|
cleared: '已清空',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
editor: '编辑器',
|
||||||
|
maxSize: '(最大 5MB)',
|
||||||
|
tree: '树形',
|
||||||
|
placeholder: '请输入或粘贴JSON数据,例如:{"name":"工具箱","version":1.0}',
|
||||||
|
jsonPathPlaceholder: '输入 JSONPath,例如: $.key.subkey',
|
||||||
|
jsonPathFilter: 'JSONPath 筛选',
|
||||||
|
clearFilter: '清除筛选',
|
||||||
|
copyFilterResult: '复制筛选结果',
|
||||||
|
expandAll: '展开全部',
|
||||||
|
collapseAll: '折叠全部',
|
||||||
|
format: '格式化',
|
||||||
|
minify: '压缩',
|
||||||
|
escape: '转义',
|
||||||
|
unescape: '取消转义',
|
||||||
|
emptyState: '在左侧输入或粘贴JSON数据,右侧将实时显示树形结构',
|
||||||
|
noMatchedNodes: '未找到匹配的节点',
|
||||||
|
noContentToCopy: '没有可复制的结果',
|
||||||
|
copiedCount: '已复制 {count} 个匹配结果',
|
||||||
|
contentOverLimit: '内容已超过 5MB 限制,已自动截断',
|
||||||
|
pleaseInputJson: '请输入JSON数据',
|
||||||
|
inputOverLimit: '输入内容超过 5MB 限制,无法格式化',
|
||||||
|
outputOverLimit: '格式化后的内容超过 5MB 限制,无法显示',
|
||||||
|
formatSuccess: '格式化成功',
|
||||||
|
jsonError: 'JSON格式错误:',
|
||||||
|
minifyOverLimit: '输入内容超过 5MB 限制,无法压缩',
|
||||||
|
minifyOutputOverLimit: '压缩后的内容超过 5MB 限制,无法显示',
|
||||||
|
minifySuccess: '压缩成功',
|
||||||
|
escapeOverLimit: '输入内容超过 5MB 限制,无法转义',
|
||||||
|
escapeOutputOverLimit: '转义后的内容超过 5MB 限制,无法显示',
|
||||||
|
escapeSuccess: '转义成功',
|
||||||
|
escapeFailed: '转义失败:',
|
||||||
|
unescapeOverLimit: '输入内容超过 5MB 限制,无法取消转义',
|
||||||
|
unescapeOutputOverLimit: '取消转义后的内容超过 5MB 限制,无法显示',
|
||||||
|
unescapeSuccess: '取消转义成功',
|
||||||
|
unescapeFormatSuccess: '取消转义并格式化成功',
|
||||||
|
unescapeFailed: '取消转义失败:',
|
||||||
|
editorEmpty: '编辑器内容为空,无法复制',
|
||||||
|
pasteOverLimit: '粘贴内容已超过 5MB 限制,已自动截断',
|
||||||
|
},
|
||||||
|
encoder: {
|
||||||
|
input: '输入',
|
||||||
|
output: '输出',
|
||||||
|
encode: '编码',
|
||||||
|
decode: '解码',
|
||||||
|
base64: 'Base64',
|
||||||
|
url: 'URL',
|
||||||
|
unicode: 'Unicode',
|
||||||
|
zlib: 'Zlib',
|
||||||
|
titleBase64: 'Base64编码',
|
||||||
|
titleUrl: 'URL编码',
|
||||||
|
titleUnicode: 'Unicode编码',
|
||||||
|
titleZlib: 'Zlib 压缩/解压',
|
||||||
|
copyOutput: '复制输出',
|
||||||
|
inputPlaceholder: '请输入要编码或解码的文本',
|
||||||
|
outputPlaceholder: '编码或解码结果将显示在这里',
|
||||||
|
pleaseInputEncode: '请输入要编码的文本',
|
||||||
|
encodeSuccess: '编码成功',
|
||||||
|
encodeFailed: '编码失败:',
|
||||||
|
pleaseInputDecode: '请输入要解码的字符串',
|
||||||
|
decodeSuccess: '解码成功',
|
||||||
|
decodeFailed: '解码失败:请检查输入是否为有效的{type}编码字符串',
|
||||||
|
inputEmptyCopy: '输入内容为空,无法复制',
|
||||||
|
copiedInput: '已复制输入到剪贴板',
|
||||||
|
outputEmptyCopy: '输出内容为空,无法复制',
|
||||||
|
copiedOutput: '已复制输出到剪贴板',
|
||||||
|
manualPaste: '请手动粘贴内容',
|
||||||
|
},
|
||||||
|
variable: {
|
||||||
|
placeholder: '请输入变量名(支持任意格式)',
|
||||||
|
camelCase: '小驼峰 (camelCase)',
|
||||||
|
pascalCase: '大驼峰 (PascalCase)',
|
||||||
|
snakeCase: '下划线 (snake_case)',
|
||||||
|
kebabCase: '横线 (kebab-case)',
|
||||||
|
constantCase: '常量 (CONSTANT_CASE)',
|
||||||
|
copyLabel: '复制{label}',
|
||||||
|
empty: '—',
|
||||||
|
noContentToCopy: '没有可复制的内容',
|
||||||
|
copiedLabel: '{label}已复制到剪贴板',
|
||||||
|
},
|
||||||
|
qr: {
|
||||||
|
inputPlaceholder: '请输入要生成二维码的内容',
|
||||||
|
generate: '生成二维码',
|
||||||
|
download: '下载',
|
||||||
|
copyImage: '复制图片',
|
||||||
|
qrCode: '二维码',
|
||||||
|
pleaseInput: '请输入要生成二维码的内容',
|
||||||
|
generateSuccess: '二维码生成成功',
|
||||||
|
generateFailed: '生成二维码失败:',
|
||||||
|
noQrToDownload: '没有可下载的二维码',
|
||||||
|
downloadSuccess: '下载成功',
|
||||||
|
downloadFailed: '下载失败:',
|
||||||
|
noQrToCopy: '没有可复制的二维码',
|
||||||
|
copyImageFailed: '复制失败,请使用下载功能',
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
dateToTs: '日期 → ({tz}) 时间戳:',
|
||||||
|
tsToDate: '时间戳 → ({tz}) 日期',
|
||||||
|
currentTs: '当前时间戳:',
|
||||||
|
placeholderTs: '请输入时间戳',
|
||||||
|
selectDateTime: '选择日期时间',
|
||||||
|
resetData: '重置数据',
|
||||||
|
resume: '继续',
|
||||||
|
pause: '暂停',
|
||||||
|
seconds: '秒',
|
||||||
|
milliseconds: '毫秒',
|
||||||
|
nanoseconds: '纳秒',
|
||||||
|
datePlaceholderSeconds: '格式:yyyy-MM-dd HH:mm:ss',
|
||||||
|
datePlaceholderMs: '格式:yyyy-MM-dd HH:mm:ss.SSS',
|
||||||
|
datePlaceholderNs: '格式:yyyy-MM-dd HH:mm:ss.SSSSSSSSS',
|
||||||
|
dataReset: '数据已重置',
|
||||||
|
invalidNs: '请输入有效的纳秒级时间戳',
|
||||||
|
invalidNumber: '请输入有效的数字',
|
||||||
|
invalidTs: '无效的时间戳',
|
||||||
|
convertFailed: '转换失败:',
|
||||||
|
invalidDateFormat: '无效的时间格式',
|
||||||
|
noContentToCopy: '没有可复制的内容',
|
||||||
|
copyFailed: '复制失败',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
rgb: 'RGB',
|
||||||
|
hex: '十六进制',
|
||||||
|
hsl: 'HSL',
|
||||||
|
copyRgb: '复制RGB',
|
||||||
|
pasteRgb: '粘贴RGB',
|
||||||
|
copyHex: '复制十六进制',
|
||||||
|
pasteHex: '粘贴十六进制',
|
||||||
|
copyHsl: '复制HSL',
|
||||||
|
pasteHsl: '粘贴HSL',
|
||||||
|
placeholderRgb: '0-255',
|
||||||
|
placeholderHex: 'FFFFFF',
|
||||||
|
placeholderH: '0-360',
|
||||||
|
placeholderSL: '0-100',
|
||||||
|
reset: '重置',
|
||||||
|
random: '随机颜色',
|
||||||
|
rgbPasted: 'RGB已粘贴并解析',
|
||||||
|
rgbOutOfRange: 'RGB值超出范围(0-255)',
|
||||||
|
hslPasted: 'HSL已粘贴并解析',
|
||||||
|
hslOutOfRange: 'HSL值超出范围(H: 0-360, S/L: 0-100)',
|
||||||
|
rgbCopied: 'RGB已复制到剪贴板',
|
||||||
|
hexCopied: '十六进制已复制到剪贴板',
|
||||||
|
hslCopied: 'HSL已复制到剪贴板',
|
||||||
|
pasteSuccess: '粘贴成功',
|
||||||
|
invalidRgb: '剪贴板内容不是有效的RGB格式',
|
||||||
|
invalidHex: '剪贴板内容不是有效的十六进制格式',
|
||||||
|
invalidHsl: '剪贴板内容不是有效的HSL格式',
|
||||||
|
manualPaste: '请手动粘贴到输入框',
|
||||||
|
pasteFailed: '粘贴失败:请手动粘贴到输入框',
|
||||||
|
},
|
||||||
|
comparator: {
|
||||||
|
textCompare: '文本对比',
|
||||||
|
jsonCompare: 'JSON对比',
|
||||||
|
lineMode: '行维度',
|
||||||
|
charMode: '字符维度',
|
||||||
|
ignoreListOrder: '忽略列表顺序',
|
||||||
|
compare: '对比',
|
||||||
|
startCompare: '开始对比',
|
||||||
|
textA: '文本 A',
|
||||||
|
textB: '文本 B',
|
||||||
|
maxSize: '(最大 {size})',
|
||||||
|
placeholderA: '请输入或粘贴要对比的内容 A',
|
||||||
|
placeholderB: '请输入或粘贴要对比的内容 B',
|
||||||
|
result: '对比结果',
|
||||||
|
same: '相同:',
|
||||||
|
insert: '插入:',
|
||||||
|
delete: '删除:',
|
||||||
|
modify: '修改:',
|
||||||
|
fullscreen: '全屏展示',
|
||||||
|
exitFullscreen: '退出全屏',
|
||||||
|
left: '左侧',
|
||||||
|
right: '右侧',
|
||||||
|
contentOverLimit: '内容已超过 {size} 限制,已自动截断',
|
||||||
|
emptyCopy: '{side}内容为空,无法复制',
|
||||||
|
copiedSide: '已复制{side}内容到剪贴板',
|
||||||
|
pasteOverLimit: '粘贴内容已超过 {size} 限制,已自动截断',
|
||||||
|
pleaseInput: '请输入要对比的内容',
|
||||||
|
inputOverLimit: '输入内容超过 {size} 限制,请减小输入大小',
|
||||||
|
largeArrayHint: '检测到大型数组,对比可能需要较长时间,请耐心等待...',
|
||||||
|
compareDone: '对比完成',
|
||||||
|
noResultToCopy: '没有对比结果可复制',
|
||||||
|
resultCopied: '已复制对比结果到剪贴板',
|
||||||
|
},
|
||||||
|
dateTimePicker: {
|
||||||
|
prevYear: '上一年',
|
||||||
|
prevMonth: '上一月',
|
||||||
|
nextMonth: '下一月',
|
||||||
|
nextYear: '下一年',
|
||||||
|
clickInputMonthYear: '点击输入年月',
|
||||||
|
placeholderMonthYear: 'YYYY-MM',
|
||||||
|
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
|
||||||
|
now: '此刻',
|
||||||
|
selectTime: '选择时间',
|
||||||
|
confirm: '确定',
|
||||||
|
cancel: '取消',
|
||||||
|
},
|
||||||
|
timestampTz: {
|
||||||
|
'UTC-12:00': '贝克岛',
|
||||||
|
'UTC-11:00': '萨摩亚',
|
||||||
|
'UTC-10:00': '夏威夷',
|
||||||
|
'UTC-09:30': '马克萨斯群岛',
|
||||||
|
'UTC-09:00': '阿拉斯加',
|
||||||
|
'UTC-08:00': '洛杉矶',
|
||||||
|
'UTC-07:00': '丹佛',
|
||||||
|
'UTC-06:00': '芝加哥',
|
||||||
|
'UTC-05:00': '纽约',
|
||||||
|
'UTC-04:00': '加拉加斯',
|
||||||
|
'UTC-03:30': '纽芬兰',
|
||||||
|
'UTC-03:00': '布宜诺斯艾利斯',
|
||||||
|
'UTC-02:00': '大西洋中部',
|
||||||
|
'UTC-01:00': '亚速尔群岛',
|
||||||
|
'UTC+00:00': '伦敦',
|
||||||
|
'UTC+01:00': '巴黎',
|
||||||
|
'UTC+02:00': '开罗',
|
||||||
|
'UTC+03:00': '莫斯科',
|
||||||
|
'UTC+03:30': '德黑兰',
|
||||||
|
'UTC+04:00': '迪拜',
|
||||||
|
'UTC+04:30': '喀布尔',
|
||||||
|
'UTC+05:00': '伊斯兰堡',
|
||||||
|
'UTC+05:30': '新德里',
|
||||||
|
'UTC+05:45': '加德满都',
|
||||||
|
'UTC+06:00': '达卡',
|
||||||
|
'UTC+06:30': '仰光',
|
||||||
|
'UTC+07:00': '曼谷',
|
||||||
|
'UTC+08:00': '北京',
|
||||||
|
'UTC+08:45': '尤克拉',
|
||||||
|
'UTC+09:00': '东京',
|
||||||
|
'UTC+09:30': '阿德莱德',
|
||||||
|
'UTC+10:00': '悉尼',
|
||||||
|
'UTC+10:30': '豪勋爵岛',
|
||||||
|
'UTC+11:00': '新喀里多尼亚',
|
||||||
|
'UTC+12:00': '奥克兰',
|
||||||
|
'UTC+12:45': '查塔姆群岛',
|
||||||
|
'UTC+13:00': '萨摩亚',
|
||||||
|
'UTC+14:00': '基里巴斯',
|
||||||
|
},
|
||||||
|
}
|
||||||
293
src/i18n/locales/zh-TW.js
Normal file
293
src/i18n/locales/zh-TW.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
export default {
|
||||||
|
nav: {
|
||||||
|
home: '首頁',
|
||||||
|
json: 'JSON',
|
||||||
|
comparator: '對比',
|
||||||
|
encoderDecoder: '編解碼',
|
||||||
|
variableName: '變數名',
|
||||||
|
qrCode: '二維碼',
|
||||||
|
timestamp: '時間戳',
|
||||||
|
color: '顏色',
|
||||||
|
},
|
||||||
|
app: {
|
||||||
|
title: 'RC707的工具箱',
|
||||||
|
titleJson: 'RC707的工具箱-JSON',
|
||||||
|
titleComparator: 'RC707的工具箱-對比',
|
||||||
|
titleEncoderDecoder: 'RC707的工具箱-編解碼',
|
||||||
|
titleVariableName: 'RC707的工具箱-變數名',
|
||||||
|
titleQrCode: 'RC707的工具箱-二維碼',
|
||||||
|
titleTimestamp: 'RC707的工具箱-時間戳',
|
||||||
|
titleColor: 'RC707的工具箱-顏色',
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
heroToday: '今天是{date}',
|
||||||
|
toolJson: 'JSON',
|
||||||
|
toolJsonDesc: '格式化、驗證和美化JSON資料',
|
||||||
|
toolComparator: '對比',
|
||||||
|
toolComparatorDesc: '文字和JSON對比工具',
|
||||||
|
toolEncoderDecoder: '編解碼',
|
||||||
|
toolEncoderDecoderDesc: '編碼/解碼工具',
|
||||||
|
toolVariableName: '變數名',
|
||||||
|
toolVariableNameDesc: '變數名格式轉換',
|
||||||
|
toolQrCode: '二維碼',
|
||||||
|
toolQrCodeDesc: '產生二維碼',
|
||||||
|
toolTimestamp: '時間戳',
|
||||||
|
toolTimestampDesc: '時間戳與時間字串相互轉換',
|
||||||
|
toolColor: '顏色',
|
||||||
|
toolColorDesc: '顏色格式轉換',
|
||||||
|
},
|
||||||
|
locale: {
|
||||||
|
zhCN: '簡體',
|
||||||
|
zhTW: '繁中',
|
||||||
|
en: 'EN',
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
close: '關閉',
|
||||||
|
copy: '複製',
|
||||||
|
paste: '貼上',
|
||||||
|
clear: '清空',
|
||||||
|
history: '歷史記錄',
|
||||||
|
noHistory: '暫無歷史記錄',
|
||||||
|
copied: '已複製到剪貼簿',
|
||||||
|
copyFailed: '複製失敗:',
|
||||||
|
clipboardEmpty: '剪貼簿內容為空',
|
||||||
|
pasteHint: '請按 Ctrl+V 或 Cmd+V 貼上內容',
|
||||||
|
pasteFailed: '無法存取編輯器,請手動貼上內容',
|
||||||
|
cleared: '已清空',
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
editor: '編輯器',
|
||||||
|
maxSize: '(最大 5MB)',
|
||||||
|
tree: '樹形',
|
||||||
|
placeholder: '請輸入或貼上JSON資料,例如:{"name":"工具箱","version":1.0}',
|
||||||
|
jsonPathPlaceholder: '輸入 JSONPath,例如: $.key.subkey',
|
||||||
|
jsonPathFilter: 'JSONPath 篩選',
|
||||||
|
clearFilter: '清除篩選',
|
||||||
|
copyFilterResult: '複製篩選結果',
|
||||||
|
expandAll: '展開全部',
|
||||||
|
collapseAll: '摺疊全部',
|
||||||
|
format: '格式化',
|
||||||
|
minify: '壓縮',
|
||||||
|
escape: '轉義',
|
||||||
|
unescape: '取消轉義',
|
||||||
|
emptyState: '在左側輸入或貼上JSON資料,右側將即時顯示樹形結構',
|
||||||
|
noMatchedNodes: '未找到符合的節點',
|
||||||
|
noContentToCopy: '沒有可複製的結果',
|
||||||
|
copiedCount: '已複製 {count} 個符合結果',
|
||||||
|
contentOverLimit: '內容已超過 5MB 限制,已自動截斷',
|
||||||
|
pleaseInputJson: '請輸入JSON資料',
|
||||||
|
inputOverLimit: '輸入內容超過 5MB 限制,無法格式化',
|
||||||
|
outputOverLimit: '格式化後的內容超過 5MB 限制,無法顯示',
|
||||||
|
formatSuccess: '格式化成功',
|
||||||
|
jsonError: 'JSON格式錯誤:',
|
||||||
|
minifyOverLimit: '輸入內容超過 5MB 限制,無法壓縮',
|
||||||
|
minifyOutputOverLimit: '壓縮後的內容超過 5MB 限制,無法顯示',
|
||||||
|
minifySuccess: '壓縮成功',
|
||||||
|
escapeOverLimit: '輸入內容超過 5MB 限制,無法轉義',
|
||||||
|
escapeOutputOverLimit: '轉義後的內容超過 5MB 限制,無法顯示',
|
||||||
|
escapeSuccess: '轉義成功',
|
||||||
|
escapeFailed: '轉義失敗:',
|
||||||
|
unescapeOverLimit: '輸入內容超過 5MB 限制,無法取消轉義',
|
||||||
|
unescapeOutputOverLimit: '取消轉義後的內容超過 5MB 限制,無法顯示',
|
||||||
|
unescapeSuccess: '取消轉義成功',
|
||||||
|
unescapeFormatSuccess: '取消轉義並格式化成功',
|
||||||
|
unescapeFailed: '取消轉義失敗:',
|
||||||
|
editorEmpty: '編輯器內容為空,無法複製',
|
||||||
|
pasteOverLimit: '貼上內容已超過 5MB 限制,已自動截斷',
|
||||||
|
},
|
||||||
|
encoder: {
|
||||||
|
input: '輸入',
|
||||||
|
output: '輸出',
|
||||||
|
encode: '編碼',
|
||||||
|
decode: '解碼',
|
||||||
|
base64: 'Base64',
|
||||||
|
url: 'URL',
|
||||||
|
unicode: 'Unicode',
|
||||||
|
zlib: 'Zlib',
|
||||||
|
titleBase64: 'Base64編碼',
|
||||||
|
titleUrl: 'URL編碼',
|
||||||
|
titleUnicode: 'Unicode編碼',
|
||||||
|
titleZlib: 'Zlib 壓縮/解壓',
|
||||||
|
copyOutput: '複製輸出',
|
||||||
|
inputPlaceholder: '請輸入要編碼或解碼的文字',
|
||||||
|
outputPlaceholder: '編碼或解碼結果將顯示在這裡',
|
||||||
|
pleaseInputEncode: '請輸入要編碼的文字',
|
||||||
|
encodeSuccess: '編碼成功',
|
||||||
|
encodeFailed: '編碼失敗:',
|
||||||
|
pleaseInputDecode: '請輸入要解碼的字串',
|
||||||
|
decodeSuccess: '解碼成功',
|
||||||
|
decodeFailed: '解碼失敗:請檢查輸入是否為有效的{type}編碼字串',
|
||||||
|
inputEmptyCopy: '輸入內容為空,無法複製',
|
||||||
|
copiedInput: '已複製輸入到剪貼簿',
|
||||||
|
outputEmptyCopy: '輸出內容為空,無法複製',
|
||||||
|
copiedOutput: '已複製輸出到剪貼簿',
|
||||||
|
manualPaste: '請手動貼上內容',
|
||||||
|
},
|
||||||
|
variable: {
|
||||||
|
placeholder: '請輸入變數名(支援任意格式)',
|
||||||
|
camelCase: '小駝峰 (camelCase)',
|
||||||
|
pascalCase: '大駝峰 (PascalCase)',
|
||||||
|
snakeCase: '底線 (snake_case)',
|
||||||
|
kebabCase: '橫線 (kebab-case)',
|
||||||
|
constantCase: '常數 (CONSTANT_CASE)',
|
||||||
|
copyLabel: '複製{label}',
|
||||||
|
empty: '—',
|
||||||
|
noContentToCopy: '沒有可複製的內容',
|
||||||
|
copiedLabel: '{label}已複製到剪貼簿',
|
||||||
|
},
|
||||||
|
qr: {
|
||||||
|
inputPlaceholder: '請輸入要產生二維碼的內容',
|
||||||
|
generate: '產生二維碼',
|
||||||
|
download: '下載',
|
||||||
|
copyImage: '複製圖片',
|
||||||
|
qrCode: '二維碼',
|
||||||
|
pleaseInput: '請輸入要產生二維碼的內容',
|
||||||
|
generateSuccess: '二維碼產生成功',
|
||||||
|
generateFailed: '產生二維碼失敗:',
|
||||||
|
noQrToDownload: '沒有可下載的二維碼',
|
||||||
|
downloadSuccess: '下載成功',
|
||||||
|
downloadFailed: '下載失敗:',
|
||||||
|
noQrToCopy: '沒有可複製的二維碼',
|
||||||
|
copyImageFailed: '複製失敗,請使用下載功能',
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
dateToTs: '日期 → ({tz}) 時間戳:',
|
||||||
|
tsToDate: '時間戳 → ({tz}) 日期',
|
||||||
|
currentTs: '目前時間戳:',
|
||||||
|
placeholderTs: '請輸入時間戳',
|
||||||
|
selectDateTime: '選擇日期時間',
|
||||||
|
resetData: '重設資料',
|
||||||
|
resume: '繼續',
|
||||||
|
pause: '暫停',
|
||||||
|
seconds: '秒',
|
||||||
|
milliseconds: '毫秒',
|
||||||
|
nanoseconds: '奈秒',
|
||||||
|
datePlaceholderSeconds: '格式:yyyy-MM-dd HH:mm:ss',
|
||||||
|
datePlaceholderMs: '格式:yyyy-MM-dd HH:mm:ss.SSS',
|
||||||
|
datePlaceholderNs: '格式:yyyy-MM-dd HH:mm:ss.SSSSSSSSS',
|
||||||
|
dataReset: '資料已重設',
|
||||||
|
invalidNs: '請輸入有效的奈秒級時間戳',
|
||||||
|
invalidNumber: '請輸入有效的數字',
|
||||||
|
invalidTs: '無效的時間戳',
|
||||||
|
convertFailed: '轉換失敗:',
|
||||||
|
invalidDateFormat: '無效的時間格式',
|
||||||
|
noContentToCopy: '沒有可複製的內容',
|
||||||
|
copyFailed: '複製失敗',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
rgb: 'RGB',
|
||||||
|
hex: '十六進位',
|
||||||
|
hsl: 'HSL',
|
||||||
|
copyRgb: '複製RGB',
|
||||||
|
pasteRgb: '貼上RGB',
|
||||||
|
copyHex: '複製十六進位',
|
||||||
|
pasteHex: '貼上十六進位',
|
||||||
|
copyHsl: '複製HSL',
|
||||||
|
pasteHsl: '貼上HSL',
|
||||||
|
placeholderRgb: '0-255',
|
||||||
|
placeholderHex: 'FFFFFF',
|
||||||
|
placeholderH: '0-360',
|
||||||
|
placeholderSL: '0-100',
|
||||||
|
reset: '重設',
|
||||||
|
random: '隨機顏色',
|
||||||
|
rgbPasted: 'RGB已貼上並解析',
|
||||||
|
rgbOutOfRange: 'RGB值超出範圍(0-255)',
|
||||||
|
hslPasted: 'HSL已貼上並解析',
|
||||||
|
hslOutOfRange: 'HSL值超出範圍(H: 0-360, S/L: 0-100)',
|
||||||
|
rgbCopied: 'RGB已複製到剪貼簿',
|
||||||
|
hexCopied: '十六進位已複製到剪貼簿',
|
||||||
|
hslCopied: 'HSL已複製到剪貼簿',
|
||||||
|
pasteSuccess: '貼上成功',
|
||||||
|
invalidRgb: '剪貼簿內容不是有效的RGB格式',
|
||||||
|
invalidHex: '剪貼簿內容不是有效的十六進位格式',
|
||||||
|
invalidHsl: '剪貼簿內容不是有效的HSL格式',
|
||||||
|
manualPaste: '請手動貼上到輸入框',
|
||||||
|
pasteFailed: '貼上失敗:請手動貼上到輸入框',
|
||||||
|
},
|
||||||
|
comparator: {
|
||||||
|
textCompare: '文字對比',
|
||||||
|
jsonCompare: 'JSON對比',
|
||||||
|
lineMode: '行維度',
|
||||||
|
charMode: '字元維度',
|
||||||
|
ignoreListOrder: '忽略列表順序',
|
||||||
|
compare: '對比',
|
||||||
|
startCompare: '開始對比',
|
||||||
|
textA: '文字 A',
|
||||||
|
textB: '文字 B',
|
||||||
|
maxSize: '(最大 {size})',
|
||||||
|
placeholderA: '請輸入或貼上要對比的內容 A',
|
||||||
|
placeholderB: '請輸入或貼上要對比的內容 B',
|
||||||
|
result: '對比結果',
|
||||||
|
same: '相同:',
|
||||||
|
insert: '插入:',
|
||||||
|
delete: '刪除:',
|
||||||
|
modify: '修改:',
|
||||||
|
fullscreen: '全螢幕展示',
|
||||||
|
exitFullscreen: '結束全螢幕',
|
||||||
|
left: '左側',
|
||||||
|
right: '右側',
|
||||||
|
contentOverLimit: '內容已超過 {size} 限制,已自動截斷',
|
||||||
|
emptyCopy: '{side}內容為空,無法複製',
|
||||||
|
copiedSide: '已複製{side}內容到剪貼簿',
|
||||||
|
pasteOverLimit: '貼上內容已超過 {size} 限制,已自動截斷',
|
||||||
|
pleaseInput: '請輸入要對比的內容',
|
||||||
|
inputOverLimit: '輸入內容超過 {size} 限制,請減小輸入大小',
|
||||||
|
largeArrayHint: '偵測到大型陣列,對比可能需要較長時間,請耐心等候...',
|
||||||
|
compareDone: '對比完成',
|
||||||
|
noResultToCopy: '沒有對比結果可複製',
|
||||||
|
resultCopied: '已複製對比結果到剪貼簿',
|
||||||
|
},
|
||||||
|
dateTimePicker: {
|
||||||
|
prevYear: '上一年',
|
||||||
|
prevMonth: '上一月',
|
||||||
|
nextMonth: '下一月',
|
||||||
|
nextYear: '下一年',
|
||||||
|
clickInputMonthYear: '點擊輸入年月',
|
||||||
|
placeholderMonthYear: 'YYYY-MM',
|
||||||
|
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
|
||||||
|
now: '此刻',
|
||||||
|
selectTime: '選擇時間',
|
||||||
|
confirm: '確定',
|
||||||
|
cancel: '取消',
|
||||||
|
},
|
||||||
|
timestampTz: {
|
||||||
|
'UTC-12:00': '貝克島',
|
||||||
|
'UTC-11:00': '薩摩亞',
|
||||||
|
'UTC-10:00': '夏威夷',
|
||||||
|
'UTC-09:30': '馬克薩斯群島',
|
||||||
|
'UTC-09:00': '阿拉斯加',
|
||||||
|
'UTC-08:00': '洛杉磯',
|
||||||
|
'UTC-07:00': '丹佛',
|
||||||
|
'UTC-06:00': '芝加哥',
|
||||||
|
'UTC-05:00': '紐約',
|
||||||
|
'UTC-04:00': '加拉加斯',
|
||||||
|
'UTC-03:30': '紐芬蘭',
|
||||||
|
'UTC-03:00': '布宜諾斯艾利斯',
|
||||||
|
'UTC-02:00': '大西洋中部',
|
||||||
|
'UTC-01:00': '亞速爾群島',
|
||||||
|
'UTC+00:00': '倫敦',
|
||||||
|
'UTC+01:00': '巴黎',
|
||||||
|
'UTC+02:00': '開羅',
|
||||||
|
'UTC+03:00': '莫斯科',
|
||||||
|
'UTC+03:30': '德黑蘭',
|
||||||
|
'UTC+04:00': '迪拜',
|
||||||
|
'UTC+04:30': '喀布爾',
|
||||||
|
'UTC+05:00': '伊斯蘭堡',
|
||||||
|
'UTC+05:30': '新德里',
|
||||||
|
'UTC+05:45': '加德滿都',
|
||||||
|
'UTC+06:00': '達卡',
|
||||||
|
'UTC+06:30': '仰光',
|
||||||
|
'UTC+07:00': '曼谷',
|
||||||
|
'UTC+08:00': '北京',
|
||||||
|
'UTC+08:45': '尤克拉',
|
||||||
|
'UTC+09:00': '東京',
|
||||||
|
'UTC+09:30': '阿德萊德',
|
||||||
|
'UTC+10:00': '悉尼',
|
||||||
|
'UTC+10:30': '豪勳爵島',
|
||||||
|
'UTC+11:00': '新喀里多尼亞',
|
||||||
|
'UTC+12:00': '奧克蘭',
|
||||||
|
'UTC+12:45': '查塔姆群島',
|
||||||
|
'UTC+13:00': '薩摩亞',
|
||||||
|
'UTC+14:00': '吉里巴斯',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import AppRoot from './AppRoot.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import { i18n } from './i18n'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
// 引入 Font Awesome 6.4
|
// 引入 Font Awesome 6.4
|
||||||
import '@fortawesome/fontawesome-free/css/all.css'
|
import '@fortawesome/fontawesome-free/css/all.css'
|
||||||
|
|
||||||
createApp(App).use(router).mount('#app')
|
createApp(AppRoot).use(router).use(i18n).mount('#app')
|
||||||
|
|
||||||
|
|||||||
@@ -1,85 +1,113 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Home from '../views/Home.vue'
|
import Home from '../views/Home.vue'
|
||||||
|
import { i18n } from '../i18n'
|
||||||
|
|
||||||
|
// 路径中的 locale 与 i18n locale 的映射
|
||||||
|
export const PATH_LOCALE_TO_I18N = {
|
||||||
|
zh: 'zh-CN',
|
||||||
|
'zh-tw': 'zh-TW',
|
||||||
|
en: 'en',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const I18N_TO_PATH_LOCALE = {
|
||||||
|
'zh-CN': 'zh',
|
||||||
|
'zh-TW': 'zh-tw',
|
||||||
|
en: 'en',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PATH_LOCALES = ['zh', 'zh-tw', 'en']
|
||||||
|
|
||||||
|
const localePath = (locale, path = '') => {
|
||||||
|
const base = path.startsWith('/') ? path.slice(1) : path
|
||||||
|
return base ? `/${locale}/${base}` : `/${locale}`
|
||||||
|
}
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
// 无前缀路径重定向到简体中文
|
||||||
|
{ path: '/', redirect: '/zh' },
|
||||||
|
{ path: '/json-formatter', redirect: '/zh/json-formatter' },
|
||||||
|
{ path: '/comparator', redirect: '/zh/comparator' },
|
||||||
|
{ path: '/encoder-decoder', redirect: '/zh/encoder-decoder' },
|
||||||
|
{ path: '/variable-name', redirect: '/zh/variable-name' },
|
||||||
|
{ path: '/qr-code', redirect: '/zh/qr-code' },
|
||||||
|
{ path: '/timestamp-converter', redirect: '/zh/timestamp-converter' },
|
||||||
|
{ path: '/color-converter', redirect: '/zh/color-converter' },
|
||||||
|
// 带语言前缀的路由(App 作为父级以获取 locale)
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/:locale(zh|zh-tw|en)',
|
||||||
name: 'Home',
|
component: () => import('../App.vue'),
|
||||||
component: Home,
|
children: [
|
||||||
meta: {
|
{
|
||||||
title: 'RC707的工具箱'
|
path: '',
|
||||||
}
|
name: 'Home',
|
||||||
|
component: Home,
|
||||||
|
meta: { titleKey: 'app.title' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'json-formatter',
|
||||||
|
name: 'JsonFormatter',
|
||||||
|
component: () => import('../views/JsonFormatter.vue'),
|
||||||
|
meta: { titleKey: 'app.titleJson' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'comparator',
|
||||||
|
name: 'Comparator',
|
||||||
|
component: () => import('../views/Comparator.vue'),
|
||||||
|
meta: { titleKey: 'app.titleComparator' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'encoder-decoder',
|
||||||
|
name: 'EncoderDecoder',
|
||||||
|
component: () => import('../views/EncoderDecoder.vue'),
|
||||||
|
meta: { titleKey: 'app.titleEncoderDecoder' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'variable-name',
|
||||||
|
name: 'VariableNameConverter',
|
||||||
|
component: () => import('../views/VariableNameConverter.vue'),
|
||||||
|
meta: { titleKey: 'app.titleVariableName' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'qr-code',
|
||||||
|
name: 'QRCodeGenerator',
|
||||||
|
component: () => import('../views/QRCodeGenerator.vue'),
|
||||||
|
meta: { titleKey: 'app.titleQrCode' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'timestamp-converter',
|
||||||
|
name: 'TimestampConverter',
|
||||||
|
component: () => import('../views/TimestampConverter.vue'),
|
||||||
|
meta: { titleKey: 'app.titleTimestamp' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'color-converter',
|
||||||
|
name: 'ColorConverter',
|
||||||
|
component: () => import('../views/ColorConverter.vue'),
|
||||||
|
meta: { titleKey: 'app.titleColor' },
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/json-formatter',
|
|
||||||
name: 'JsonFormatter',
|
|
||||||
component: () => import('../views/JsonFormatter.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-JSON'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/comparator',
|
|
||||||
name: 'Comparator',
|
|
||||||
component: () => import('../views/Comparator.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-对比'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/encoder-decoder',
|
|
||||||
name: 'EncoderDecoder',
|
|
||||||
component: () => import('../views/EncoderDecoder.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-编解码'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/variable-name',
|
|
||||||
name: 'VariableNameConverter',
|
|
||||||
component: () => import('../views/VariableNameConverter.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-变量名'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/qr-code',
|
|
||||||
name: 'QRCodeGenerator',
|
|
||||||
component: () => import('../views/QRCodeGenerator.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-二维码'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/timestamp-converter',
|
|
||||||
name: 'TimestampConverter',
|
|
||||||
component: () => import('../views/TimestampConverter.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-时间戳'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/color-converter',
|
|
||||||
name: 'ColorConverter',
|
|
||||||
component: () => import('../views/ColorConverter.vue'),
|
|
||||||
meta: {
|
|
||||||
title: 'RC707的工具箱-颜色'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 路由守卫:设置页面标题
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
if (to.meta.title) {
|
const pathLocale = to.params.locale
|
||||||
document.title = to.meta.title
|
if (pathLocale && PATH_LOCALE_TO_I18N[pathLocale]) {
|
||||||
|
const i18nLocale = PATH_LOCALE_TO_I18N[pathLocale]
|
||||||
|
if (i18n.global.locale.value !== i18nLocale) {
|
||||||
|
i18n.global.locale.value = i18nLocale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const key = to.meta.titleKey
|
||||||
|
if (key) {
|
||||||
|
document.title = i18n.global.t(key)
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export { localePath }
|
||||||
export default router
|
export default router
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<i v-else class="fas fa-circle-info"></i>
|
<i v-else class="fas fa-circle-info"></i>
|
||||||
<span>{{ toastMessage }}</span>
|
<span>{{ toastMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="closeToast" class="toast-close-btn" title="关闭">
|
<button @click="closeToast" class="toast-close-btn" :title="t('common.close')">
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
<!-- 左侧侧栏(历史记录) -->
|
<!-- 左侧侧栏(历史记录) -->
|
||||||
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h3>历史记录</h3>
|
<h3>{{ t('common.history') }}</h3>
|
||||||
<button @click="toggleSidebar" class="close-btn">×</button>
|
<button @click="toggleSidebar" class="close-btn">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<div v-if="historyList.length === 0" class="empty-history">
|
<div v-if="historyList.length === 0" class="empty-history">
|
||||||
暂无历史记录
|
{{ t('common.noHistory') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in historyList"
|
v-for="(item, index) in historyList"
|
||||||
@@ -50,12 +50,12 @@
|
|||||||
<!-- RGB输入 -->
|
<!-- RGB输入 -->
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-header">
|
<div class="input-header">
|
||||||
<label class="input-label">RGB</label>
|
<label class="input-label">{{ t('color.rgb') }}</label>
|
||||||
<div class="copy-paste-buttons">
|
<div class="copy-paste-buttons">
|
||||||
<button @click="copyRgb" class="copy-btn" title="复制RGB">
|
<button @click="copyRgb" class="copy-btn" :title="t('color.copyRgb')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="pasteRgb" class="paste-btn" title="粘贴RGB">
|
<button @click="pasteRgb" class="paste-btn" :title="t('color.pasteRgb')">
|
||||||
<i class="far fa-paste"></i>
|
<i class="far fa-paste"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="255"
|
max="255"
|
||||||
class="rgb-input"
|
class="rgb-input"
|
||||||
placeholder="0-255"
|
:placeholder="t('color.placeholderRgb')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="rgb-item">
|
<div class="rgb-item">
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="255"
|
max="255"
|
||||||
class="rgb-input"
|
class="rgb-input"
|
||||||
placeholder="0-255"
|
:placeholder="t('color.placeholderRgb')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="rgb-item">
|
<div class="rgb-item">
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="255"
|
max="255"
|
||||||
class="rgb-input"
|
class="rgb-input"
|
||||||
placeholder="0-255"
|
:placeholder="t('color.placeholderRgb')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,12 +106,12 @@
|
|||||||
<!-- 十六进制输入 -->
|
<!-- 十六进制输入 -->
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-header">
|
<div class="input-header">
|
||||||
<label class="input-label">十六进制</label>
|
<label class="input-label">{{ t('color.hex') }}</label>
|
||||||
<div class="copy-paste-buttons">
|
<div class="copy-paste-buttons">
|
||||||
<button @click="copyHex" class="copy-btn" title="复制十六进制">
|
<button @click="copyHex" class="copy-btn" :title="t('color.copyHex')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="pasteHex" class="paste-btn" title="粘贴十六进制">
|
<button @click="pasteHex" class="paste-btn" :title="t('color.pasteHex')">
|
||||||
<i class="far fa-paste"></i>
|
<i class="far fa-paste"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
@input="handleHexInput"
|
@input="handleHexInput"
|
||||||
type="text"
|
type="text"
|
||||||
class="hex-input"
|
class="hex-input"
|
||||||
placeholder="FFFFFF"
|
:placeholder="t('color.placeholderHex')"
|
||||||
maxlength="6"
|
maxlength="6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,12 +132,12 @@
|
|||||||
<!-- HSL输入 -->
|
<!-- HSL输入 -->
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-header">
|
<div class="input-header">
|
||||||
<label class="input-label">HSL</label>
|
<label class="input-label">{{ t('color.hsl') }}</label>
|
||||||
<div class="copy-paste-buttons">
|
<div class="copy-paste-buttons">
|
||||||
<button @click="copyHsl" class="copy-btn" title="复制HSL">
|
<button @click="copyHsl" class="copy-btn" :title="t('color.copyHsl')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="pasteHsl" class="paste-btn" title="粘贴HSL">
|
<button @click="pasteHsl" class="paste-btn" :title="t('color.pasteHsl')">
|
||||||
<i class="far fa-paste"></i>
|
<i class="far fa-paste"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="360"
|
max="360"
|
||||||
class="hsl-input"
|
class="hsl-input"
|
||||||
placeholder="0-360"
|
:placeholder="t('color.placeholderH')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="hsl-item">
|
<div class="hsl-item">
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
class="hsl-input"
|
class="hsl-input"
|
||||||
placeholder="0-100"
|
:placeholder="t('color.placeholderSL')"
|
||||||
/>
|
/>
|
||||||
<span class="hsl-unit">%</span>
|
<span class="hsl-unit">%</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
class="hsl-input"
|
class="hsl-input"
|
||||||
placeholder="0-100"
|
:placeholder="t('color.placeholderSL')"
|
||||||
/>
|
/>
|
||||||
<span class="hsl-unit">%</span>
|
<span class="hsl-unit">%</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,8 +199,8 @@
|
|||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button @click="resetColor" class="action-btn reset-btn">重置</button>
|
<button @click="resetColor" class="action-btn reset-btn">{{ t('color.reset') }}</button>
|
||||||
<button @click="randomColor" class="action-btn random-btn">随机颜色</button>
|
<button @click="randomColor" class="action-btn random-btn">{{ t('color.random') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,6 +210,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const rgb = ref({ r: 255, g: 255, b: 255 })
|
const rgb = ref({ r: 255, g: 255, b: 255 })
|
||||||
const hex = ref('FFFFFF')
|
const hex = ref('FFFFFF')
|
||||||
@@ -257,7 +260,13 @@ const currentColor = computed(() => {
|
|||||||
return `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`
|
return `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`
|
||||||
})
|
})
|
||||||
|
|
||||||
// RGB转十六进制
|
/**
|
||||||
|
* RGB转十六进制
|
||||||
|
* @param {number} r - 红色值 (0-255)
|
||||||
|
* @param {number} g - 绿色值 (0-255)
|
||||||
|
* @param {number} b - 蓝色值 (0-255)
|
||||||
|
* @returns {string} 十六进制颜色值(不含#号)
|
||||||
|
*/
|
||||||
function rgbToHex(r, g, b) {
|
function rgbToHex(r, g, b) {
|
||||||
const toHex = (n) => {
|
const toHex = (n) => {
|
||||||
const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16)
|
const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16)
|
||||||
@@ -266,7 +275,13 @@ function rgbToHex(r, g, b) {
|
|||||||
return (toHex(r) + toHex(g) + toHex(b)).toUpperCase()
|
return (toHex(r) + toHex(g) + toHex(b)).toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RGB转HSL
|
/**
|
||||||
|
* RGB转HSL
|
||||||
|
* @param {number} r - 红色值 (0-255)
|
||||||
|
* @param {number} g - 绿色值 (0-255)
|
||||||
|
* @param {number} b - 蓝色值 (0-255)
|
||||||
|
* @returns {{h: number, s: number, l: number}} HSL对象
|
||||||
|
*/
|
||||||
function rgbToHsl(r, g, b) {
|
function rgbToHsl(r, g, b) {
|
||||||
r /= 255
|
r /= 255
|
||||||
g /= 255
|
g /= 255
|
||||||
@@ -302,7 +317,11 @@ function rgbToHsl(r, g, b) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 十六进制转RGB
|
/**
|
||||||
|
* 十六进制转RGB
|
||||||
|
* @param {string} hex - 十六进制颜色值(不含#号)
|
||||||
|
* @returns {{r: number, g: number, b: number}|null} RGB对象或null
|
||||||
|
*/
|
||||||
function hexToRgb(hex) {
|
function hexToRgb(hex) {
|
||||||
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
return result
|
return result
|
||||||
@@ -314,7 +333,13 @@ function hexToRgb(hex) {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
// HSL转RGB
|
/**
|
||||||
|
* HSL转RGB
|
||||||
|
* @param {number} h - 色相 (0-360)
|
||||||
|
* @param {number} s - 饱和度 (0-100)
|
||||||
|
* @param {number} l - 亮度 (0-100)
|
||||||
|
* @returns {{r: number, g: number, b: number}} RGB对象
|
||||||
|
*/
|
||||||
function hslToRgb(h, s, l) {
|
function hslToRgb(h, s, l) {
|
||||||
h /= 360
|
h /= 360
|
||||||
s /= 100
|
s /= 100
|
||||||
@@ -385,9 +410,9 @@ function handleRgbPaste(event) {
|
|||||||
// 保存到历史记录
|
// 保存到历史记录
|
||||||
saveToHistory()
|
saveToHistory()
|
||||||
|
|
||||||
showToast('RGB已粘贴并解析', 'info', 2000)
|
showToast(t('color.rgbPasted'), 'info', 2000)
|
||||||
} else {
|
} else {
|
||||||
showToast('RGB值超出范围(0-255)', 'error')
|
showToast(t('color.rgbOutOfRange'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果不是RGB格式,允许默认粘贴行为(粘贴单个数字)
|
// 如果不是RGB格式,允许默认粘贴行为(粘贴单个数字)
|
||||||
@@ -505,9 +530,9 @@ function handleHslPaste(event) {
|
|||||||
// 保存到历史记录
|
// 保存到历史记录
|
||||||
saveToHistory()
|
saveToHistory()
|
||||||
|
|
||||||
showToast('HSL已粘贴并解析', 'info', 2000)
|
showToast(t('color.hslPasted'), 'info', 2000)
|
||||||
} else {
|
} else {
|
||||||
showToast('HSL值超出范围(H: 0-360, S/L: 0-100)', 'error')
|
showToast(t('color.hslOutOfRange'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果不是HSL格式,允许默认粘贴行为(粘贴单个数字)
|
// 如果不是HSL格式,允许默认粘贴行为(粘贴单个数字)
|
||||||
@@ -566,17 +591,16 @@ async function copyRgb() {
|
|||||||
const rgbText = `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`
|
const rgbText = `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(rgbText)
|
await navigator.clipboard.writeText(rgbText)
|
||||||
showToast('RGB已复制到剪贴板', 'info', 2000)
|
showToast(t('color.rgbCopied'), 'info', 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('复制失败:', err)
|
showToast(t('common.copyFailed') + err.message)
|
||||||
showToast('复制失败:' + err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理粘贴的文本(通用函数)
|
// 处理粘贴的文本(通用函数)
|
||||||
function processPastedText(text, type) {
|
function processPastedText(text, type) {
|
||||||
if (!text || !text.trim()) {
|
if (!text || !text.trim()) {
|
||||||
showToast('剪贴板内容为空')
|
showToast(t('common.clipboardEmpty'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,11 +619,11 @@ function processPastedText(text, type) {
|
|||||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
||||||
rgb.value = { r, g, b }
|
rgb.value = { r, g, b }
|
||||||
handleRgbInput()
|
handleRgbInput()
|
||||||
showToast('粘贴成功', 'info', 2000)
|
showToast(t('color.pasteSuccess'), 'info', 2000)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showToast('剪贴板内容不是有效的RGB格式')
|
showToast(t('color.invalidRgb'))
|
||||||
return false
|
return false
|
||||||
} else if (type === 'hex') {
|
} else if (type === 'hex') {
|
||||||
// 移除#号并转换为大写
|
// 移除#号并转换为大写
|
||||||
@@ -609,16 +633,16 @@ function processPastedText(text, type) {
|
|||||||
if (/^[0-9A-F]{6}$/.test(text)) {
|
if (/^[0-9A-F]{6}$/.test(text)) {
|
||||||
hex.value = text
|
hex.value = text
|
||||||
handleHexInput()
|
handleHexInput()
|
||||||
showToast('粘贴成功', 'info', 2000)
|
showToast(t('color.pasteSuccess'), 'info', 2000)
|
||||||
return true
|
return true
|
||||||
} else if (/^[0-9A-F]{3}$/.test(text)) {
|
} else if (/^[0-9A-F]{3}$/.test(text)) {
|
||||||
// 支持3位十六进制
|
// 支持3位十六进制
|
||||||
hex.value = text.split('').map(c => c + c).join('')
|
hex.value = text.split('').map(c => c + c).join('')
|
||||||
handleHexInput()
|
handleHexInput()
|
||||||
showToast('粘贴成功', 'info', 2000)
|
showToast(t('color.pasteSuccess'), 'info', 2000)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
showToast('剪贴板内容不是有效的十六进制格式')
|
showToast(t('color.invalidHex'))
|
||||||
return false
|
return false
|
||||||
} else if (type === 'hsl') {
|
} else if (type === 'hsl') {
|
||||||
// 支持格式:hsl(0, 0%, 100%) 或 0, 0%, 100%
|
// 支持格式:hsl(0, 0%, 100%) 或 0, 0%, 100%
|
||||||
@@ -633,11 +657,11 @@ function processPastedText(text, type) {
|
|||||||
if (h >= 0 && h <= 360 && s >= 0 && s <= 100 && l >= 0 && l <= 100) {
|
if (h >= 0 && h <= 360 && s >= 0 && s <= 100 && l >= 0 && l <= 100) {
|
||||||
hsl.value = { h, s, l }
|
hsl.value = { h, s, l }
|
||||||
handleHslInput()
|
handleHslInput()
|
||||||
showToast('粘贴成功', 'info', 2000)
|
showToast(t('color.pasteSuccess'), 'info', 2000)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showToast('剪贴板内容不是有效的HSL格式')
|
showToast(t('color.invalidHsl'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -651,15 +675,14 @@ async function pasteRgb() {
|
|||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
processPastedText(text, 'rgb')
|
processPastedText(text, 'rgb')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('粘贴失败:', err)
|
|
||||||
// Clipboard API 失败,使用备用方法
|
// Clipboard API 失败,使用备用方法
|
||||||
currentPasteType.value = 'rgb'
|
currentPasteType.value = 'rgb'
|
||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
if (pasteInputRef.value) {
|
if (pasteInputRef.value) {
|
||||||
pasteInputRef.value.focus()
|
pasteInputRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
} else {
|
} else {
|
||||||
showToast('粘贴失败:请手动粘贴到输入框', 'error')
|
showToast(t('color.pasteFailed'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -668,9 +691,9 @@ async function pasteRgb() {
|
|||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
if (pasteInputRef.value) {
|
if (pasteInputRef.value) {
|
||||||
pasteInputRef.value.focus()
|
pasteInputRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
} else {
|
} else {
|
||||||
showToast('请手动粘贴到输入框', 'error')
|
showToast(t('color.manualPaste'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,10 +703,9 @@ async function copyHex() {
|
|||||||
const hexText = `#${hex.value}`
|
const hexText = `#${hex.value}`
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(hexText)
|
await navigator.clipboard.writeText(hexText)
|
||||||
showToast('十六进制已复制到剪贴板', 'info', 2000)
|
showToast(t('color.hexCopied'), 'info', 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('复制失败:', err)
|
showToast(t('common.copyFailed') + err.message)
|
||||||
showToast('复制失败:' + err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,15 +717,14 @@ async function pasteHex() {
|
|||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
processPastedText(text, 'hex')
|
processPastedText(text, 'hex')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('粘贴失败:', err)
|
|
||||||
// Clipboard API 失败,使用备用方法
|
// Clipboard API 失败,使用备用方法
|
||||||
currentPasteType.value = 'hex'
|
currentPasteType.value = 'hex'
|
||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
if (pasteInputRef.value) {
|
if (pasteInputRef.value) {
|
||||||
pasteInputRef.value.focus()
|
pasteInputRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
} else {
|
} else {
|
||||||
showToast('粘贴失败:请手动粘贴到输入框', 'error')
|
showToast(t('color.pasteFailed'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -712,9 +733,9 @@ async function pasteHex() {
|
|||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
if (pasteInputRef.value) {
|
if (pasteInputRef.value) {
|
||||||
pasteInputRef.value.focus()
|
pasteInputRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
} else {
|
} else {
|
||||||
showToast('请手动粘贴到输入框', 'error')
|
showToast(t('color.manualPaste'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -724,10 +745,9 @@ async function copyHsl() {
|
|||||||
const hslText = `hsl(${hsl.value.h}, ${hsl.value.s}%, ${hsl.value.l}%)`
|
const hslText = `hsl(${hsl.value.h}, ${hsl.value.s}%, ${hsl.value.l}%)`
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(hslText)
|
await navigator.clipboard.writeText(hslText)
|
||||||
showToast('HSL已复制到剪贴板', 'info', 2000)
|
showToast(t('color.hslCopied'), 'info', 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('复制失败:', err)
|
showToast(t('common.copyFailed') + err.message)
|
||||||
showToast('复制失败:' + err.message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,15 +759,14 @@ async function pasteHsl() {
|
|||||||
const text = await navigator.clipboard.readText()
|
const text = await navigator.clipboard.readText()
|
||||||
processPastedText(text, 'hsl')
|
processPastedText(text, 'hsl')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('粘贴失败:', err)
|
|
||||||
// Clipboard API 失败,使用备用方法
|
// Clipboard API 失败,使用备用方法
|
||||||
currentPasteType.value = 'hsl'
|
currentPasteType.value = 'hsl'
|
||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
if (pasteInputRef.value) {
|
if (pasteInputRef.value) {
|
||||||
pasteInputRef.value.focus()
|
pasteInputRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
} else {
|
} else {
|
||||||
showToast('粘贴失败:请手动粘贴到输入框', 'error')
|
showToast(t('color.pasteFailed'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -756,9 +775,9 @@ async function pasteHsl() {
|
|||||||
pasteInputValue.value = ''
|
pasteInputValue.value = ''
|
||||||
if (pasteInputRef.value) {
|
if (pasteInputRef.value) {
|
||||||
pasteInputRef.value.focus()
|
pasteInputRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
} else {
|
} else {
|
||||||
showToast('请手动粘贴到输入框', 'error')
|
showToast(t('color.manualPaste'), 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -779,7 +798,10 @@ function handlePasteEvent(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到历史记录
|
/**
|
||||||
|
* 保存当前颜色到历史记录
|
||||||
|
* 自动去重,最多保存50条记录
|
||||||
|
*/
|
||||||
function saveToHistory() {
|
function saveToHistory() {
|
||||||
const colorData = {
|
const colorData = {
|
||||||
rgb: `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`,
|
rgb: `rgb(${rgb.value.r}, ${rgb.value.g}, ${rgb.value.b})`,
|
||||||
@@ -803,7 +825,7 @@ function saveToHistory() {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('读取历史记录失败', e)
|
// 读取历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否与最后一条历史记录相同
|
// 检查是否与最后一条历史记录相同
|
||||||
@@ -828,11 +850,13 @@ function saveToHistory() {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存历史记录失败', e)
|
// 保存历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载历史记录列表
|
/**
|
||||||
|
* 从localStorage加载历史记录列表
|
||||||
|
*/
|
||||||
function loadHistoryList() {
|
function loadHistoryList() {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
@@ -840,12 +864,15 @@ function loadHistoryList() {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载历史记录失败', e)
|
// 加载历史记录失败,重置为空数组
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载历史记录
|
/**
|
||||||
|
* 加载历史记录到编辑器
|
||||||
|
* @param {Object} colorData - 颜色数据对象
|
||||||
|
*/
|
||||||
function loadHistory(colorData) {
|
function loadHistory(colorData) {
|
||||||
updating.value = true
|
updating.value = true
|
||||||
rgb.value = { ...colorData.rgbValues }
|
rgb.value = { ...colorData.rgbValues }
|
||||||
@@ -860,7 +887,11 @@ function toggleSidebar() {
|
|||||||
sidebarOpen.value = !sidebarOpen.value
|
sidebarOpen.value = !sidebarOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化时间
|
/**
|
||||||
|
* 格式化时间戳为相对时间或日期字符串
|
||||||
|
* @param {number} timestamp - 时间戳(毫秒)
|
||||||
|
* @returns {string} 格式化后的时间字符串
|
||||||
|
*/
|
||||||
function formatTime(timestamp) {
|
function formatTime(timestamp) {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<i v-else class="fas fa-circle-info"></i>
|
<i v-else class="fas fa-circle-info"></i>
|
||||||
<span>{{ toastMessage }}</span>
|
<span>{{ toastMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="closeToast" class="toast-close-btn" title="关闭">
|
<button @click="closeToast" class="toast-close-btn" :title="t('common.close')">
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,12 +18,12 @@
|
|||||||
<!-- 左侧侧栏(历史记录) -->
|
<!-- 左侧侧栏(历史记录) -->
|
||||||
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h3>历史记录</h3>
|
<h3>{{ t('common.history') }}</h3>
|
||||||
<button @click="toggleSidebar" class="close-btn">×</button>
|
<button @click="toggleSidebar" class="close-btn">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<div v-if="historyList.length === 0" class="empty-history">
|
<div v-if="historyList.length === 0" class="empty-history">
|
||||||
暂无历史记录
|
{{ t('common.noHistory') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in historyList"
|
v-for="(item, index) in historyList"
|
||||||
@@ -48,46 +48,46 @@
|
|||||||
<button
|
<button
|
||||||
@click="compareMode = 'text'"
|
@click="compareMode = 'text'"
|
||||||
:class="['mode-btn', { active: compareMode === 'text' }]"
|
:class="['mode-btn', { active: compareMode === 'text' }]"
|
||||||
title="文本对比"
|
:title="t('comparator.textCompare')"
|
||||||
>
|
>
|
||||||
文本对比
|
{{ t('comparator.textCompare') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="compareMode = 'json'"
|
@click="compareMode = 'json'"
|
||||||
:class="['mode-btn', { active: compareMode === 'json' }]"
|
:class="['mode-btn', { active: compareMode === 'json' }]"
|
||||||
title="JSON对比"
|
:title="t('comparator.jsonCompare')"
|
||||||
>
|
>
|
||||||
JSON对比
|
{{ t('comparator.jsonCompare') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="submode-selector" v-if="compareMode === 'text'">
|
<div class="submode-selector" v-if="compareMode === 'text'">
|
||||||
<button
|
<button
|
||||||
@click="textSubMode = 'line'"
|
@click="textSubMode = 'line'"
|
||||||
:class="['submode-btn', { active: textSubMode === 'line' }]"
|
:class="['submode-btn', { active: textSubMode === 'line' }]"
|
||||||
title="按行对比"
|
:title="t('comparator.lineMode')"
|
||||||
>
|
>
|
||||||
行维度
|
{{ t('comparator.lineMode') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="textSubMode = 'char'"
|
@click="textSubMode = 'char'"
|
||||||
:class="['submode-btn', { active: textSubMode === 'char' }]"
|
:class="['submode-btn', { active: textSubMode === 'char' }]"
|
||||||
title="按字符对比"
|
:title="t('comparator.charMode')"
|
||||||
>
|
>
|
||||||
字符维度
|
{{ t('comparator.charMode') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="submode-selector" v-if="compareMode === 'json'">
|
<div class="submode-selector" v-if="compareMode === 'json'">
|
||||||
<label class="switch-label">
|
<label class="switch-label">
|
||||||
<input type="checkbox" v-model="ignoreListOrder" class="switch-input" />
|
<input type="checkbox" v-model="ignoreListOrder" class="switch-input" />
|
||||||
<span class="switch-text">忽略列表顺序</span>
|
<span class="switch-text">{{ t('comparator.ignoreListOrder') }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<button @click="performCompare" class="action-btn primary" title="开始对比">
|
<button @click="performCompare" class="action-btn primary" :title="t('comparator.startCompare')">
|
||||||
<i class="fas fa-code-compare"></i>
|
<i class="fas fa-code-compare"></i>
|
||||||
<span>对比</span>
|
<span>{{ t('comparator.compare') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button @click="clearAll" class="action-btn" title="清空">
|
<button @click="clearAll" class="action-btn" :title="t('common.clear')">
|
||||||
<i class="far fa-trash-can"></i>
|
<i class="far fa-trash-can"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,15 +98,15 @@
|
|||||||
<!-- 左侧输入框 -->
|
<!-- 左侧输入框 -->
|
||||||
<div class="input-panel" :style="{ width: leftPanelWidth + '%' }">
|
<div class="input-panel" :style="{ width: leftPanelWidth + '%' }">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-label">文本 A</span>
|
<span class="panel-label">{{ t('comparator.textA') }} <span class="size-limit">{{ t('comparator.maxSize', { size: maxInputLabel }) }}</span></span>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button @click="copyToClipboard('left')" class="icon-btn" title="复制">
|
<button @click="copyToClipboard('left')" class="icon-btn" :title="t('common.copy')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="pasteFromClipboard('left')" class="icon-btn" title="粘贴">
|
<button @click="pasteFromClipboard('left')" class="icon-btn" :title="t('common.paste')">
|
||||||
<i class="far fa-paste"></i>
|
<i class="far fa-paste"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="clearInput('left')" class="icon-btn" title="清空">
|
<button @click="clearInput('left')" class="icon-btn" :title="t('common.clear')">
|
||||||
<i class="far fa-trash-can"></i>
|
<i class="far fa-trash-can"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
ref="leftEditorRef"
|
ref="leftEditorRef"
|
||||||
v-model="leftText"
|
v-model="leftText"
|
||||||
@input="updateLeftLineCount"
|
@input="updateLeftLineCount"
|
||||||
placeholder="请输入或粘贴要对比的内容 A"
|
:placeholder="t('comparator.placeholderA')"
|
||||||
class="text-editor"
|
class="text-editor"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,15 +139,15 @@
|
|||||||
<!-- 右侧输入框 -->
|
<!-- 右侧输入框 -->
|
||||||
<div class="input-panel" :style="{ width: rightPanelWidth + '%' }">
|
<div class="input-panel" :style="{ width: rightPanelWidth + '%' }">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-label">文本 B</span>
|
<span class="panel-label">{{ t('comparator.textB') }} <span class="size-limit">{{ t('comparator.maxSize', { size: maxInputLabel }) }}</span></span>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button @click="copyToClipboard('right')" class="icon-btn" title="复制">
|
<button @click="copyToClipboard('right')" class="icon-btn" :title="t('common.copy')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="pasteFromClipboard('right')" class="icon-btn" title="粘贴">
|
<button @click="pasteFromClipboard('right')" class="icon-btn" :title="t('common.paste')">
|
||||||
<i class="far fa-paste"></i>
|
<i class="far fa-paste"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="clearInput('right')" class="icon-btn" title="清空">
|
<button @click="clearInput('right')" class="icon-btn" :title="t('common.clear')">
|
||||||
<i class="far fa-trash-can"></i>
|
<i class="far fa-trash-can"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
ref="rightEditorRef"
|
ref="rightEditorRef"
|
||||||
v-model="rightText"
|
v-model="rightText"
|
||||||
@input="updateRightLineCount"
|
@input="updateRightLineCount"
|
||||||
placeholder="请输入或粘贴要对比的内容 B"
|
:placeholder="t('comparator.placeholderB')"
|
||||||
class="text-editor"
|
class="text-editor"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,26 +170,26 @@
|
|||||||
<!-- 对比结果区域 -->
|
<!-- 对比结果区域 -->
|
||||||
<div class="result-container" v-if="compareResult">
|
<div class="result-container" v-if="compareResult">
|
||||||
<div class="result-header">
|
<div class="result-header">
|
||||||
<span class="result-label">对比结果</span>
|
<span class="result-label">{{ t('comparator.result') }}</span>
|
||||||
<div class="result-stats">
|
<div class="result-stats">
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">相同:</span>
|
<span class="stat-label">{{ t('comparator.same') }}</span>
|
||||||
<span class="stat-value same">{{ compareResult.stats.same }}</span>
|
<span class="stat-value same">{{ compareResult.stats.same }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">插入:</span>
|
<span class="stat-label">{{ t('comparator.insert') }}</span>
|
||||||
<span class="stat-value insert">{{ compareResult.stats.insert }}</span>
|
<span class="stat-value insert">{{ compareResult.stats.insert }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">删除:</span>
|
<span class="stat-label">{{ t('comparator.delete') }}</span>
|
||||||
<span class="stat-value delete">{{ compareResult.stats.delete }}</span>
|
<span class="stat-value delete">{{ compareResult.stats.delete }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">修改:</span>
|
<span class="stat-label">{{ t('comparator.modify') }}</span>
|
||||||
<span class="stat-value modify">{{ compareResult.stats.modify }}</span>
|
<span class="stat-value modify">{{ compareResult.stats.modify }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="resultFullscreen = true" class="icon-btn" title="全屏展示">
|
<button @click="resultFullscreen = true" class="icon-btn" :title="t('comparator.fullscreen')">
|
||||||
<i class="fas fa-expand"></i>
|
<i class="fas fa-expand"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -223,26 +223,26 @@
|
|||||||
<div v-if="resultFullscreen" class="result-fullscreen-overlay">
|
<div v-if="resultFullscreen" class="result-fullscreen-overlay">
|
||||||
<div class="result-fullscreen-inner">
|
<div class="result-fullscreen-inner">
|
||||||
<div class="result-fullscreen-header">
|
<div class="result-fullscreen-header">
|
||||||
<span class="result-label">对比结果</span>
|
<span class="result-label">{{ t('comparator.result') }}</span>
|
||||||
<div class="result-stats" v-if="compareResult">
|
<div class="result-stats" v-if="compareResult">
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">相同:</span>
|
<span class="stat-label">{{ t('comparator.same') }}</span>
|
||||||
<span class="stat-value same">{{ compareResult.stats.same }}</span>
|
<span class="stat-value same">{{ compareResult.stats.same }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">插入:</span>
|
<span class="stat-label">{{ t('comparator.insert') }}</span>
|
||||||
<span class="stat-value insert">{{ compareResult.stats.insert }}</span>
|
<span class="stat-value insert">{{ compareResult.stats.insert }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">删除:</span>
|
<span class="stat-label">{{ t('comparator.delete') }}</span>
|
||||||
<span class="stat-value delete">{{ compareResult.stats.delete }}</span>
|
<span class="stat-value delete">{{ compareResult.stats.delete }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">修改:</span>
|
<span class="stat-label">{{ t('comparator.modify') }}</span>
|
||||||
<span class="stat-value modify">{{ compareResult.stats.modify }}</span>
|
<span class="stat-value modify">{{ compareResult.stats.modify }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="resultFullscreen = false" class="icon-btn" title="退出全屏">
|
<button @click="resultFullscreen = false" class="icon-btn" :title="t('comparator.exitFullscreen')">
|
||||||
<i class="fas fa-compress"></i>
|
<i class="fas fa-compress"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,9 +274,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref, computed, watch, onMounted, onUnmounted} from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const MAX_INPUT_BYTES = 500 * 1024 // 最大 500KB
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// JSON对比模式:最大 2MB(受算法复杂度限制,O(n*m) 动态规划)
|
||||||
|
// 文本对比模式:最大 5MB(算法复杂度较低)
|
||||||
|
const MAX_INPUT_BYTES_JSON = 2 * 1024 * 1024 // 2MB for JSON comparison
|
||||||
|
const MAX_INPUT_BYTES_TEXT = 5 * 1024 * 1024 // 5MB for text comparison
|
||||||
|
|
||||||
const leftText = ref('')
|
const leftText = ref('')
|
||||||
const rightText = ref('')
|
const rightText = ref('')
|
||||||
@@ -311,6 +317,15 @@ const leftFullscreenResultRef = ref(null)
|
|||||||
const rightFullscreenResultRef = ref(null)
|
const rightFullscreenResultRef = ref(null)
|
||||||
const isScrolling = ref(false)
|
const isScrolling = ref(false)
|
||||||
|
|
||||||
|
// 计算当前模式的最大输入限制
|
||||||
|
const maxInputBytes = computed(() => {
|
||||||
|
return compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxInputLabel = computed(() => {
|
||||||
|
return compareMode.value === 'json' ? '2MB' : '5MB'
|
||||||
|
})
|
||||||
|
|
||||||
// 提示消息
|
// 提示消息
|
||||||
const toastMessage = ref('')
|
const toastMessage = ref('')
|
||||||
const toastType = ref('error')
|
const toastType = ref('error')
|
||||||
@@ -353,9 +368,12 @@ const truncateToMaxBytes = (str, maxBytes) => {
|
|||||||
// 应用输入大小限制,超出则截断并提示
|
// 应用输入大小限制,超出则截断并提示
|
||||||
const applyInputLimit = (side) => {
|
const applyInputLimit = (side) => {
|
||||||
const ref = side === 'left' ? leftText : rightText
|
const ref = side === 'left' ? leftText : rightText
|
||||||
if (getByteLength(ref.value) <= MAX_INPUT_BYTES) return
|
const maxBytes = compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
|
||||||
ref.value = truncateToMaxBytes(ref.value, MAX_INPUT_BYTES)
|
const maxBytesLabel = compareMode.value === 'json' ? '2MB' : '5MB'
|
||||||
showToast(`内容已超过 500KB 限制,已自动截断`, 'info', 3000)
|
|
||||||
|
if (getByteLength(ref.value) <= maxBytes) return
|
||||||
|
ref.value = truncateToMaxBytes(ref.value, maxBytes)
|
||||||
|
showToast(t('comparator.contentOverLimit', { size: maxBytesLabel }), 'info', 3000)
|
||||||
if (side === 'left') updateLeftLineCount()
|
if (side === 'left') updateLeftLineCount()
|
||||||
else updateRightLineCount()
|
else updateRightLineCount()
|
||||||
}
|
}
|
||||||
@@ -414,14 +432,14 @@ const stopResize = () => {
|
|||||||
const copyToClipboard = async (side) => {
|
const copyToClipboard = async (side) => {
|
||||||
const text = side === 'left' ? leftText.value : rightText.value
|
const text = side === 'left' ? leftText.value : rightText.value
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
showToast(`${side === 'left' ? '左侧' : '右侧'}内容为空,无法复制`)
|
showToast(t('comparator.emptyCopy', { side: side === 'left' ? t('comparator.left') : t('comparator.right') }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
showToast(`已复制${side === 'left' ? '左侧' : '右侧'}内容到剪贴板`, 'info', 2000)
|
showToast(t('comparator.copiedSide', { side: side === 'left' ? t('comparator.left') : t('comparator.right') }), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('复制失败:' + e.message)
|
showToast(t('common.copyFailed') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,9 +449,12 @@ const pasteFromClipboard = async (side) => {
|
|||||||
try {
|
try {
|
||||||
let text = await navigator.clipboard.readText()
|
let text = await navigator.clipboard.readText()
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
if (getByteLength(text) > MAX_INPUT_BYTES) {
|
const maxBytes = compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
|
||||||
text = truncateToMaxBytes(text, MAX_INPUT_BYTES)
|
const maxBytesLabel = compareMode.value === 'json' ? '2MB' : '5MB'
|
||||||
showToast('粘贴内容已超过 500KB 限制,已自动截断', 'info', 3000)
|
|
||||||
|
if (getByteLength(text) > maxBytes) {
|
||||||
|
text = truncateToMaxBytes(text, maxBytes)
|
||||||
|
showToast(t('comparator.pasteOverLimit', { size: maxBytesLabel }), 'info', 3000)
|
||||||
}
|
}
|
||||||
if (side === 'left') {
|
if (side === 'left') {
|
||||||
leftText.value = text
|
leftText.value = text
|
||||||
@@ -443,13 +464,13 @@ const pasteFromClipboard = async (side) => {
|
|||||||
updateRightLineCount()
|
updateRightLineCount()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast('剪贴板内容为空')
|
showToast(t('common.clipboardEmpty'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const editorRef = side === 'left' ? leftEditorRef.value : rightEditorRef.value
|
const editorRef = side === 'left' ? leftEditorRef.value : rightEditorRef.value
|
||||||
if (editorRef) {
|
if (editorRef) {
|
||||||
editorRef.focus()
|
editorRef.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,7 +494,7 @@ const clearAll = () => {
|
|||||||
compareResult.value = null
|
compareResult.value = null
|
||||||
updateLeftLineCount()
|
updateLeftLineCount()
|
||||||
updateRightLineCount()
|
updateRightLineCount()
|
||||||
showToast('已清空', 'info', 2000)
|
showToast(t('common.cleared'), 'info', 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转义HTML
|
// 转义HTML
|
||||||
@@ -1271,7 +1292,6 @@ const compareListsIgnoreOrder = (listA, listB, ignoreOrder = false) => {
|
|||||||
|
|
||||||
// 对比List(数组)- 寻找最佳匹配使相似度最高
|
// 对比List(数组)- 寻找最佳匹配使相似度最高
|
||||||
const compareLists = (listA, listB, ignoreOrder = false) => {
|
const compareLists = (listA, listB, ignoreOrder = false) => {
|
||||||
|
|
||||||
const n = listA.length
|
const n = listA.length
|
||||||
const m = listB.length
|
const m = listB.length
|
||||||
|
|
||||||
@@ -1693,11 +1713,9 @@ const compareJson = (textA, textB) => {
|
|||||||
|
|
||||||
// 执行节点对比,传递忽略列表顺序的参数
|
// 执行节点对比,传递忽略列表顺序的参数
|
||||||
const comparison = compareJsonNodes(jsonA, jsonB, ignoreListOrder.value)
|
const comparison = compareJsonNodes(jsonA, jsonB, ignoreListOrder.value)
|
||||||
console.log(comparison)
|
|
||||||
|
|
||||||
// 转换为展示格式
|
// 转换为展示格式
|
||||||
const displayResult = convertComparisonToDisplay(comparison, 0)
|
const displayResult = convertComparisonToDisplay(comparison, 0)
|
||||||
console.log(displayResult)
|
|
||||||
|
|
||||||
// 计算统计信息
|
// 计算统计信息
|
||||||
const stats = calculateStats(comparison)
|
const stats = calculateStats(comparison)
|
||||||
@@ -1736,10 +1754,52 @@ const compareJson = (textA, textB) => {
|
|||||||
// 执行对比
|
// 执行对比
|
||||||
const performCompare = () => {
|
const performCompare = () => {
|
||||||
if (!leftText.value.trim() && !rightText.value.trim()) {
|
if (!leftText.value.trim() && !rightText.value.trim()) {
|
||||||
showToast('请输入要对比的内容')
|
showToast(t('comparator.pleaseInput'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查输入大小限制
|
||||||
|
const maxBytes = compareMode.value === 'json' ? MAX_INPUT_BYTES_JSON : MAX_INPUT_BYTES_TEXT
|
||||||
|
const maxBytesLabel = compareMode.value === 'json' ? '2MB' : '5MB'
|
||||||
|
const leftBytes = getByteLength(leftText.value)
|
||||||
|
const rightBytes = getByteLength(rightText.value)
|
||||||
|
|
||||||
|
if (leftBytes > maxBytes || rightBytes > maxBytes) {
|
||||||
|
showToast(t('comparator.inputOverLimit', { size: maxBytesLabel }), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON对比模式:检查是否可能因复杂度过高导致性能问题
|
||||||
|
if (compareMode.value === 'json') {
|
||||||
|
try {
|
||||||
|
const jsonA = JSON.parse(leftText.value)
|
||||||
|
const jsonB = JSON.parse(rightText.value)
|
||||||
|
|
||||||
|
// 检查是否有大型数组(可能影响性能)
|
||||||
|
const checkLargeArray = (obj, depth = 0) => {
|
||||||
|
if (depth > 10) return false // 防止无限递归
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
if (obj.length > 1000) return true
|
||||||
|
// 检查嵌套数组
|
||||||
|
for (const item of obj.slice(0, 10)) {
|
||||||
|
if (checkLargeArray(item, depth + 1)) return true
|
||||||
|
}
|
||||||
|
} else if (obj && typeof obj === 'object') {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (checkLargeArray(obj[key], depth + 1)) return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkLargeArray(jsonA) || checkLargeArray(jsonB)) {
|
||||||
|
showToast(t('comparator.largeArrayHint'), 'info', 5000)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// JSON解析失败会在下面处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (compareMode.value === 'json') {
|
if (compareMode.value === 'json') {
|
||||||
compareResult.value = compareJson(leftText.value, rightText.value)
|
compareResult.value = compareJson(leftText.value, rightText.value)
|
||||||
@@ -1753,7 +1813,7 @@ const performCompare = () => {
|
|||||||
}
|
}
|
||||||
// 保存到历史记录
|
// 保存到历史记录
|
||||||
saveToHistory()
|
saveToHistory()
|
||||||
showToast('对比完成', 'info', 2000)
|
showToast(t('comparator.compareDone'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.message)
|
showToast(e.message)
|
||||||
compareResult.value = null
|
compareResult.value = null
|
||||||
@@ -1763,7 +1823,7 @@ const performCompare = () => {
|
|||||||
// 复制结果
|
// 复制结果
|
||||||
const copyResult = async () => {
|
const copyResult = async () => {
|
||||||
if (!compareResult.value) {
|
if (!compareResult.value) {
|
||||||
showToast('没有对比结果可复制')
|
showToast(t('comparator.noResultToCopy'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1772,83 +1832,12 @@ const copyResult = async () => {
|
|||||||
const rightContent = compareResult.value.right.map(l => l.content).join('\n')
|
const rightContent = compareResult.value.right.map(l => l.content).join('\n')
|
||||||
const result = `文本A:\n${leftContent}\n\n文本B:\n${rightContent}`
|
const result = `文本A:\n${leftContent}\n\n文本B:\n${rightContent}`
|
||||||
await navigator.clipboard.writeText(result)
|
await navigator.clipboard.writeText(result)
|
||||||
showToast('已复制对比结果到剪贴板', 'info', 2000)
|
showToast(t('comparator.resultCopied'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('复制失败:' + e.message)
|
showToast(t('common.copyFailed') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const compareTest = () => {
|
|
||||||
let total = {
|
|
||||||
left: [],
|
|
||||||
right: [],
|
|
||||||
stats: {same: 0, insert: 0, delete: 0, modify: 0}
|
|
||||||
}
|
|
||||||
const cases = [
|
|
||||||
["[]","[\"a\"]"],
|
|
||||||
["[\"a\"]", "[]"],
|
|
||||||
["[[\"a\",\"b\"]]", "[]"],
|
|
||||||
["[]","[\"a\",\"b\"]"],
|
|
||||||
["[]","[[\"a\",\"b\"]]"],
|
|
||||||
["{}","{\"a\": {\"c\":\"d\"}}"],
|
|
||||||
["{}", "{\"a\":\"b\"}"],
|
|
||||||
["[\"a\",\"b\"]", "[]"],
|
|
||||||
["{\"a\": {\"c\":\"d\"}}", "[]"],
|
|
||||||
["{\"a\":\"b\"}", "{}"],
|
|
||||||
["{\"a\":\"a\",\"b\":\"b\",\"d\":null}", "{\"a\":\"a\",\"c\":\"c\"}"],
|
|
||||||
["[\"a\",\"b\",null]", "[\"a\",\"c\"]"],
|
|
||||||
["[{\"a\":\"a\"},{\"b\":\"b\"},null]", "[{\"a\":\"a\"},{\"c\":\"c\"}]"],
|
|
||||||
["{\"a\":\"a\",\"b\":\"b\",\"c\":null}", "{\"a\":\"a\",\"c\":\"c\"}"],
|
|
||||||
["[{\"c\":\"d\"}]","[[\"c\",\"d\"]]"],
|
|
||||||
["{\"a\": {\"c\":\"d\"}}","{\"a\": [\"c\",\"d\"]}"],
|
|
||||||
["{\"a\": {\"c\":\"d\"}}","[\"c\",\"d\"]"],
|
|
||||||
["{\"a\": {\"c\":\"d\"}}","{\"a\": null}"],
|
|
||||||
["{\"a\": null}","{\"a\": {\"c\":\"d\"}}"],
|
|
||||||
["{\"a\": {\"c\":\"d\"}}","{}"],
|
|
||||||
["{\"a\":\"a1\"}", "{\"b\":\"b2\"}"],
|
|
||||||
["{\"a\":{\"c\":\"d\",\"e\":\"f\"}}", "{\"b\":{\"c\":\"d\",\"m\":\"l\"}}"],
|
|
||||||
["{\"a\":{\"c\":\"d\",\"e\":\"f\"}}", "{\"a\":{\"c\":\"d\",\"e\":\"l\"}}"],
|
|
||||||
["{\"a\": {\"c\":\"d\"}}", "{\"a\":[\"c\",\"d\"]}"],
|
|
||||||
["[{\"a\":\"a1\"},{\"b\":\"b2\"}]", "[{\"c\":\"b1\"},{\"a\":\"a1\"}]"],
|
|
||||||
["[{\"a\":\"a1\"},{\"b\":\"b2\"}]", "[{\"b\":\"b1\"},{\"a\":\"a1\"}]"],
|
|
||||||
["[{\"a\":\"a1\"},{\"b\":\"b2\"}]", "[{\"c\":\"b1\"},{\"a\":\"a1\"}]"],
|
|
||||||
["{\"a\":{\"c\":\"d\",\"e\":\"f\"},\"c\":\"a\",\"d\":\"e\",\"g\":[\"a\",\"b\",\"c\"],\"h\":[\"a\"],\"i\":[{\"a\":\"a1\"},{\"b\":\"b2\"},{\"c\":\"b1\"}]}", "{\"b\":{\"c\":\"d\",\"m\":\"l\"},\"c\":\"a\",\"d\":\"f\",\"g\":[\"a\",\"c\",\"b\"],\"h\":[\"b\",\"a\"],\"i\":[{\"b\":\"b1\"},{\"a\":\"a1\"},{\"d\":\"b1\"}]}"],
|
|
||||||
["[[[\"a\",\"b\"],[]]]","[[[\"a\",\"b\",\"c\"],[\"b\"]],[\"c\"]]"]
|
|
||||||
]
|
|
||||||
for (let item of cases) {
|
|
||||||
console.log("=================================================")
|
|
||||||
console.log(item)
|
|
||||||
console.log("=================================================")
|
|
||||||
let res = compareJson(item[0], item[1])
|
|
||||||
total.left.push({type: 'same', content: ''}, {
|
|
||||||
type: 'same',
|
|
||||||
content: '====================================='
|
|
||||||
}, {type: 'same', content: ''}, {type: 'same', content: item[0]}, {type: 'same', content: ''}, {
|
|
||||||
type: 'same',
|
|
||||||
content: '-----------------------------------'
|
|
||||||
})
|
|
||||||
total.right.push({type: 'same', content: ''}, {
|
|
||||||
type: 'same',
|
|
||||||
content: '====================================='
|
|
||||||
}, {type: 'same', content: ''}, {type: 'same', content: item[1]}, {type: 'same', content: ''}, {
|
|
||||||
type: 'same',
|
|
||||||
content: '-----------------------------------'
|
|
||||||
})
|
|
||||||
total.left.push({type: 'same', content: JSON.stringify(res.stats)}, {
|
|
||||||
type: 'same',
|
|
||||||
content: '-----------------------------------'
|
|
||||||
})
|
|
||||||
total.right.push({type: 'same', content: JSON.stringify(res.stats)}, {
|
|
||||||
type: 'same',
|
|
||||||
content: '-----------------------------------'
|
|
||||||
})
|
|
||||||
total.left.push(...res.left)
|
|
||||||
total.right.push(...res.right)
|
|
||||||
}
|
|
||||||
compareResult.value = total
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换侧栏
|
// 切换侧栏
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
sidebarOpen.value = !sidebarOpen.value
|
sidebarOpen.value = !sidebarOpen.value
|
||||||
@@ -1912,7 +1901,7 @@ const saveToHistory = () => {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('读取历史记录失败', e)
|
// 读取历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否与最新记录相同,如果相同则不保存
|
// 检查是否与最新记录相同,如果相同则不保存
|
||||||
@@ -1940,7 +1929,7 @@ const saveToHistory = () => {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存历史记录失败', e)
|
// 保存历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1952,7 +1941,7 @@ const loadHistoryList = () => {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载历史记录失败', e)
|
// 加载历史记录失败,忽略错误
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1973,7 +1962,6 @@ onMounted(() => {
|
|||||||
updateLeftLineCount()
|
updateLeftLineCount()
|
||||||
updateRightLineCount()
|
updateRightLineCount()
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
// compareTest()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -2467,6 +2455,13 @@ onUnmounted(() => {
|
|||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-label .size-limit {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #999999;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-actions {
|
.panel-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<i v-else class="fas fa-circle-info"></i>
|
<i v-else class="fas fa-circle-info"></i>
|
||||||
<span>{{ toastMessage }}</span>
|
<span>{{ toastMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="closeToast" class="toast-close-btn" title="关闭">
|
<button @click="closeToast" class="toast-close-btn" :title="t('common.close')">
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,12 +18,12 @@
|
|||||||
<!-- 左侧侧栏(历史记录) -->
|
<!-- 左侧侧栏(历史记录) -->
|
||||||
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h3>历史记录</h3>
|
<h3>{{ t('common.history') }}</h3>
|
||||||
<button @click="toggleSidebar" class="close-btn">×</button>
|
<button @click="toggleSidebar" class="close-btn">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<div v-if="historyList.length === 0" class="empty-history">
|
<div v-if="historyList.length === 0" class="empty-history">
|
||||||
暂无历史记录
|
{{ t('common.noHistory') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in historyList"
|
v-for="(item, index) in historyList"
|
||||||
@@ -32,13 +32,8 @@
|
|||||||
@click="loadHistory(item)"
|
@click="loadHistory(item)"
|
||||||
>
|
>
|
||||||
<div class="history-header">
|
<div class="history-header">
|
||||||
<span class="history-type">{{ item.type === 'encode' ? '编码' : '解码' }}</span>
|
<span class="history-type">{{ item.type === 'encode' ? t('encoder.encode') : t('encoder.decode') }}</span>
|
||||||
<span class="history-encoding">{{
|
<span class="history-encoding">{{ encodingTypeLabel(item.encodingType) }}</span>
|
||||||
item.encodingType === 'base64' ? 'Base64' :
|
|
||||||
item.encodingType === 'url' ? 'URL' :
|
|
||||||
item.encodingType === 'unicode' ? 'Unicode' :
|
|
||||||
item.encodingType
|
|
||||||
}}</span>
|
|
||||||
<span class="history-time">{{ formatTime(item.time) }}</span>
|
<span class="history-time">{{ formatTime(item.time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-preview">{{ truncateText(item.input, 50) }}</div>
|
<div class="history-preview">{{ truncateText(item.input, 50) }}</div>
|
||||||
@@ -52,45 +47,52 @@
|
|||||||
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
|
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
|
||||||
<div class="panel-toolbar">
|
<div class="panel-toolbar">
|
||||||
<div class="view-tabs">
|
<div class="view-tabs">
|
||||||
<button class="view-tab active">输入</button>
|
<button class="view-tab active">{{ t('encoder.input') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<div class="encoding-type-selector">
|
<div class="encoding-type-selector">
|
||||||
<button
|
<button
|
||||||
@click="encodingType = 'base64'"
|
@click="encodingType = 'base64'"
|
||||||
:class="['type-btn', { active: encodingType === 'base64' }]"
|
:class="['type-btn', { active: encodingType === 'base64' }]"
|
||||||
title="Base64编码"
|
:title="t('encoder.titleBase64')"
|
||||||
>
|
>
|
||||||
Base64
|
{{ t('encoder.base64') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="encodingType = 'url'"
|
@click="encodingType = 'url'"
|
||||||
:class="['type-btn', { active: encodingType === 'url' }]"
|
:class="['type-btn', { active: encodingType === 'url' }]"
|
||||||
title="URL编码"
|
:title="t('encoder.titleUrl')"
|
||||||
>
|
>
|
||||||
URL
|
{{ t('encoder.url') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="encodingType = 'unicode'"
|
@click="encodingType = 'unicode'"
|
||||||
:class="['type-btn', { active: encodingType === 'unicode' }]"
|
:class="['type-btn', { active: encodingType === 'unicode' }]"
|
||||||
title="Unicode编码"
|
:title="t('encoder.titleUnicode')"
|
||||||
>
|
>
|
||||||
Unicode
|
{{ t('encoder.unicode') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="encodingType = 'zlib'"
|
||||||
|
:class="['type-btn', { active: encodingType === 'zlib' }]"
|
||||||
|
:title="t('encoder.titleZlib')"
|
||||||
|
>
|
||||||
|
{{ t('encoder.zlib') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button @click="copyInputToClipboard" class="toolbar-icon-btn" title="复制">
|
<button @click="copyInputToClipboard" class="toolbar-icon-btn" :title="t('common.copy')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="pasteFromClipboard" class="toolbar-icon-btn" title="粘贴">
|
<button @click="pasteFromClipboard" class="toolbar-icon-btn" :title="t('common.paste')">
|
||||||
<i class="far fa-paste"></i>
|
<i class="far fa-paste"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="encode" class="toolbar-icon-btn" title="编码">
|
<button @click="encode" class="toolbar-icon-btn" :title="t('encoder.encode')">
|
||||||
<i class="fa-solid fa-code"></i>
|
<i class="fa-solid fa-code"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="decode" class="toolbar-icon-btn" title="解码">
|
<button @click="decode" class="toolbar-icon-btn" :title="t('encoder.decode')">
|
||||||
<svg viewBox="150 -50 1100 1250" xmlns="http://www.w3.org/2000/svg" width="17" height="17"><path d="M285.352637 0.003641h111.956538v114.687184h-111.956538v282.621991a110.682235 110.682235 0 0 1-33.313896 81.282266 110.682235 110.682235 0 0 1-81.282266 33.313896 110.682235 110.682235 0 0 1 81.282266 33.313897 110.682235 110.682235 0 0 1 33.313896 81.282266v282.621991h111.956538v114.687184h-111.956538a188.050574 188.050574 0 0 1-80.007964-40.049493 93.570179 93.570179 0 0 1-34.67922-74.637691v-226.643722a110.682235 110.682235 0 0 0-33.313896-81.282267 110.682235 110.682235 0 0 0-81.282267-33.313896H0v-111.956537h55.978269a110.682235 110.682235 0 0 0 81.282266-33.313897 110.682235 110.682235 0 0 0 33.313896-81.282266V114.690825A113.776969 113.776969 0 0 1 285.261616 0.003641z m794.61835 0a113.776969 113.776969 0 0 1 114.687184 114.687184v226.643722a113.776969 113.776969 0 0 0 114.687185 114.687184H1365.323624v111.956538h-55.978268a113.776969 113.776969 0 0 0-114.687185 114.687184v226.643722a113.776969 113.776969 0 0 1-114.687184 114.687184h-111.956537V909.309175h111.956537V626.687184a113.776969 113.776969 0 0 1 114.687184-114.687184 113.776969 113.776969 0 0 1-114.687184-114.687184V114.690825h-111.956537V0.003641h111.956537zM682.661812 682.665453a54.612945 54.612945 0 0 1 55.978269 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 54.612945 54.612945 0 0 1-80.007965 0 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z m-226.643721 0a54.612945 54.612945 0 0 1 55.978268 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 52.246384 52.246384 0 0 1-40.049493 17.294099 59.164024 59.164024 0 0 1-58.708916-58.708916 52.246384 52.246384 0 0 1 17.294099-40.049493 58.799937 58.799937 0 0 1 41.505839-15.837754z m453.287443 0a58.799937 58.799937 0 0 1 41.323795 16.019797 52.246384 52.246384 0 0 1 17.294099 40.049493 59.164024 59.164024 0 0 1-58.708916 58.708916 52.246384 52.246384 0 0 1-40.049493-17.294099 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z" fill="#666666" p-id="26339"></path></svg>
|
<svg viewBox="150 -50 1100 1250" xmlns="http://www.w3.org/2000/svg" width="17" height="17"><path d="M285.352637 0.003641h111.956538v114.687184h-111.956538v282.621991a110.682235 110.682235 0 0 1-33.313896 81.282266 110.682235 110.682235 0 0 1-81.282266 33.313896 110.682235 110.682235 0 0 1 81.282266 33.313897 110.682235 110.682235 0 0 1 33.313896 81.282266v282.621991h111.956538v114.687184h-111.956538a188.050574 188.050574 0 0 1-80.007964-40.049493 93.570179 93.570179 0 0 1-34.67922-74.637691v-226.643722a110.682235 110.682235 0 0 0-33.313896-81.282267 110.682235 110.682235 0 0 0-81.282267-33.313896H0v-111.956537h55.978269a110.682235 110.682235 0 0 0 81.282266-33.313897 110.682235 110.682235 0 0 0 33.313896-81.282266V114.690825A113.776969 113.776969 0 0 1 285.261616 0.003641z m794.61835 0a113.776969 113.776969 0 0 1 114.687184 114.687184v226.643722a113.776969 113.776969 0 0 0 114.687185 114.687184H1365.323624v111.956538h-55.978268a113.776969 113.776969 0 0 0-114.687185 114.687184v226.643722a113.776969 113.776969 0 0 1-114.687184 114.687184h-111.956537V909.309175h111.956537V626.687184a113.776969 113.776969 0 0 1 114.687184-114.687184 113.776969 113.776969 0 0 1-114.687184-114.687184V114.690825h-111.956537V0.003641h111.956537zM682.661812 682.665453a54.612945 54.612945 0 0 1 55.978269 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 54.612945 54.612945 0 0 1-80.007965 0 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z m-226.643721 0a54.612945 54.612945 0 0 1 55.978268 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 52.246384 52.246384 0 0 1-40.049493 17.294099 59.164024 59.164024 0 0 1-58.708916-58.708916 52.246384 52.246384 0 0 1 17.294099-40.049493 58.799937 58.799937 0 0 1 41.505839-15.837754z m453.287443 0a58.799937 58.799937 0 0 1 41.323795 16.019797 52.246384 52.246384 0 0 1 17.294099 40.049493 59.164024 59.164024 0 0 1-58.708916 58.708916 52.246384 52.246384 0 0 1-40.049493-17.294099 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z" fill="#666666" p-id="26339"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
<button @click="clearAll" class="toolbar-icon-btn" title="清空">
|
<button @click="clearAll" class="toolbar-icon-btn" :title="t('common.clear')">
|
||||||
<i class="far fa-trash-can"></i>
|
<i class="far fa-trash-can"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +105,7 @@
|
|||||||
ref="inputEditorRef"
|
ref="inputEditorRef"
|
||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
@input="updateInputLineCount"
|
@input="updateInputLineCount"
|
||||||
placeholder="请输入要编码或解码的文本"
|
:placeholder="t('encoder.inputPlaceholder')"
|
||||||
class="text-editor"
|
class="text-editor"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,10 +126,10 @@
|
|||||||
<div class="right-panel" :style="{ width: rightPanelWidth + '%' }">
|
<div class="right-panel" :style="{ width: rightPanelWidth + '%' }">
|
||||||
<div class="panel-toolbar">
|
<div class="panel-toolbar">
|
||||||
<div class="view-tabs">
|
<div class="view-tabs">
|
||||||
<button class="view-tab active">输出</button>
|
<button class="view-tab active">{{ t('encoder.output') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<button @click="copyOutputToClipboard" class="toolbar-icon-btn" title="复制输出">
|
<button @click="copyOutputToClipboard" class="toolbar-icon-btn" :title="t('encoder.copyOutput')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +142,7 @@
|
|||||||
v-model="outputText"
|
v-model="outputText"
|
||||||
readonly
|
readonly
|
||||||
class="text-editor output-editor"
|
class="text-editor output-editor"
|
||||||
placeholder="编码或解码结果将显示在这里"
|
:placeholder="t('encoder.outputPlaceholder')"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,11 +152,23 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
function encodingTypeLabel(type) {
|
||||||
|
if (type === 'base64') return t('encoder.base64')
|
||||||
|
if (type === 'url') return t('encoder.url')
|
||||||
|
if (type === 'unicode') return t('encoder.unicode')
|
||||||
|
if (type === 'zlib') return t('encoder.zlib')
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
import { zlibSync, decompressSync } from 'fflate'
|
||||||
|
|
||||||
const inputText = ref('')
|
const inputText = ref('')
|
||||||
const outputText = ref('')
|
const outputText = ref('')
|
||||||
const encodingType = ref('base64') // 'base64'、'url' 或 'unicode'
|
const encodingType = ref('base64') // 'base64'、'url'、'unicode' 或 'zlib'
|
||||||
const leftPanelWidth = ref(50)
|
const leftPanelWidth = ref(50)
|
||||||
const rightPanelWidth = ref(50)
|
const rightPanelWidth = ref(50)
|
||||||
const isResizing = ref(false)
|
const isResizing = ref(false)
|
||||||
@@ -219,6 +233,26 @@ watch(() => outputText.value, () => {
|
|||||||
updateOutputLineCount()
|
updateOutputLineCount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Zlib: Uint8Array -> Base64(用于显示/复制)
|
||||||
|
const bytesToBase64 = (bytes) => {
|
||||||
|
let binary = ''
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i])
|
||||||
|
}
|
||||||
|
return btoa(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zlib: Base64 -> Uint8Array(自动去除空白字符)
|
||||||
|
const base64ToBytes = (str) => {
|
||||||
|
const clean = str.replace(/\s/g, '')
|
||||||
|
const binary = atob(clean)
|
||||||
|
const bytes = new Uint8Array(binary.length)
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i)
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
// Unicode 编码
|
// Unicode 编码
|
||||||
const encodeUnicode = (text) => {
|
const encodeUnicode = (text) => {
|
||||||
let result = ''
|
let result = ''
|
||||||
@@ -310,7 +344,7 @@ const decodeUnicode = (text) => {
|
|||||||
|
|
||||||
const encode = () => {
|
const encode = () => {
|
||||||
if (!inputText.value.trim()) {
|
if (!inputText.value.trim()) {
|
||||||
showToast('请输入要编码的文本')
|
showToast(t('encoder.pleaseInputEncode'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -321,6 +355,11 @@ const encode = () => {
|
|||||||
result = encodeURIComponent(inputText.value)
|
result = encodeURIComponent(inputText.value)
|
||||||
} else if (encodingType.value === 'unicode') {
|
} else if (encodingType.value === 'unicode') {
|
||||||
result = encodeUnicode(inputText.value)
|
result = encodeUnicode(inputText.value)
|
||||||
|
} else if (encodingType.value === 'zlib') {
|
||||||
|
const utf8 = new TextEncoder().encode(inputText.value)
|
||||||
|
// 使用 zlib 格式,与 Java Deflater/Inflater 默认格式兼容
|
||||||
|
const compressed = zlibSync(utf8)
|
||||||
|
result = bytesToBase64(compressed)
|
||||||
}
|
}
|
||||||
outputText.value = result
|
outputText.value = result
|
||||||
|
|
||||||
@@ -332,16 +371,16 @@ const encode = () => {
|
|||||||
output: result
|
output: result
|
||||||
})
|
})
|
||||||
|
|
||||||
showToast('编码成功', 'info', 2000)
|
showToast(t('encoder.encodeSuccess'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('编码失败:' + e.message)
|
showToast(t('encoder.encodeFailed') + e.message)
|
||||||
outputText.value = ''
|
outputText.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const decode = () => {
|
const decode = () => {
|
||||||
if (!inputText.value.trim()) {
|
if (!inputText.value.trim()) {
|
||||||
showToast('请输入要解码的字符串')
|
showToast(t('encoder.pleaseInputDecode'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -352,6 +391,11 @@ const decode = () => {
|
|||||||
result = decodeURIComponent(inputText.value)
|
result = decodeURIComponent(inputText.value)
|
||||||
} else if (encodingType.value === 'unicode') {
|
} else if (encodingType.value === 'unicode') {
|
||||||
result = decodeUnicode(inputText.value)
|
result = decodeUnicode(inputText.value)
|
||||||
|
} else if (encodingType.value === 'zlib') {
|
||||||
|
const compressed = base64ToBytes(inputText.value.trim())
|
||||||
|
// decompressSync 自动识别 zlib / raw deflate / gzip,可解 Java Deflater 输出
|
||||||
|
const decompressed = decompressSync(compressed)
|
||||||
|
result = new TextDecoder().decode(decompressed)
|
||||||
}
|
}
|
||||||
outputText.value = result
|
outputText.value = result
|
||||||
|
|
||||||
@@ -363,13 +407,10 @@ const decode = () => {
|
|||||||
output: result
|
output: result
|
||||||
})
|
})
|
||||||
|
|
||||||
showToast('解码成功', 'info', 2000)
|
showToast(t('encoder.decodeSuccess'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const typeName = encodingType.value === 'base64' ? 'Base64' :
|
const typeName = encodingTypeLabel(encodingType.value)
|
||||||
encodingType.value === 'url' ? 'URL' :
|
showToast(t('encoder.decodeFailed', { type: typeName }))
|
||||||
encodingType.value === 'unicode' ? 'Unicode' :
|
|
||||||
encodingType.value
|
|
||||||
showToast(`解码失败:请检查输入是否为有效的${typeName}编码字符串`)
|
|
||||||
outputText.value = ''
|
outputText.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,34 +420,34 @@ const clearAll = () => {
|
|||||||
outputText.value = ''
|
outputText.value = ''
|
||||||
inputLineCount.value = 1
|
inputLineCount.value = 1
|
||||||
outputLineCount.value = 1
|
outputLineCount.value = 1
|
||||||
showToast('已清空', 'info', 2000)
|
showToast(t('common.cleared'), 'info', 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制输入到剪贴板
|
// 复制输入到剪贴板
|
||||||
const copyInputToClipboard = async () => {
|
const copyInputToClipboard = async () => {
|
||||||
if (!inputText.value.trim()) {
|
if (!inputText.value.trim()) {
|
||||||
showToast('输入内容为空,无法复制')
|
showToast(t('encoder.inputEmptyCopy'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(inputText.value)
|
await navigator.clipboard.writeText(inputText.value)
|
||||||
showToast('已复制输入到剪贴板', 'info', 2000)
|
showToast(t('encoder.copiedInput'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('复制失败:' + e.message)
|
showToast(t('common.copyFailed') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制输出到剪贴板
|
// 复制输出到剪贴板
|
||||||
const copyOutputToClipboard = async () => {
|
const copyOutputToClipboard = async () => {
|
||||||
if (!outputText.value.trim()) {
|
if (!outputText.value.trim()) {
|
||||||
showToast('输出内容为空,无法复制')
|
showToast(t('encoder.outputEmptyCopy'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(outputText.value)
|
await navigator.clipboard.writeText(outputText.value)
|
||||||
showToast('已复制输出到剪贴板', 'info', 2000)
|
showToast(t('encoder.copiedOutput'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('复制失败:' + e.message)
|
showToast(t('common.copyFailed') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,20 +460,20 @@ const pasteFromClipboard = async () => {
|
|||||||
inputText.value = text
|
inputText.value = text
|
||||||
updateInputLineCount()
|
updateInputLineCount()
|
||||||
} else {
|
} else {
|
||||||
showToast('剪贴板内容为空')
|
showToast(t('common.clipboardEmpty'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (inputEditorRef.value) {
|
if (inputEditorRef.value) {
|
||||||
inputEditorRef.value.focus()
|
inputEditorRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
} else {
|
} else {
|
||||||
showToast('无法访问编辑器,请手动粘贴内容')
|
showToast(t('encoder.manualPaste'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (inputEditorRef.value) {
|
if (inputEditorRef.value) {
|
||||||
inputEditorRef.value.focus()
|
inputEditorRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,7 +525,7 @@ const saveToHistory = (item) => {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('读取历史记录失败', e)
|
// 读取历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复保存相同的记录
|
// 避免重复保存相同的记录
|
||||||
@@ -510,7 +551,7 @@ const saveToHistory = (item) => {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存历史记录失败', e)
|
// 保存历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,7 +563,7 @@ const loadHistoryList = () => {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载历史记录失败', e)
|
// 加载历史记录失败,重置为空数组
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-container">
|
<div class="home-container">
|
||||||
<div class="hero-section">
|
<div class="hero-section">
|
||||||
<h2 class="hero-title">今天是{{`${new Date().getFullYear()}年${new Date().getMonth()+1}月${new Date().getDate()}日`}}</h2>
|
<h2 class="hero-title">
|
||||||
<p class="hero-subtitle">凭君莫话封侯事,一将功成万骨枯。</p>
|
{{ t('home.heroToday', { date: heroDate }) }}
|
||||||
|
</h2>
|
||||||
|
<p id="jinrishici-sentence" class="hero-subtitle"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="tools-grid">
|
<div class="tools-grid">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="tool in tools"
|
v-for="tool in tools"
|
||||||
:key="tool.path"
|
:key="tool.path"
|
||||||
:to="tool.path"
|
:to="localePath(currentPathLocale, tool.path)"
|
||||||
class="tool-card"
|
class="tool-card"
|
||||||
>
|
>
|
||||||
<h3 class="tool-title">{{ tool.title }}</h3>
|
<h3 class="tool-title">{{ tool.title }}</h3>
|
||||||
@@ -22,45 +24,43 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { onMounted, computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { localePath } from '../router'
|
||||||
|
|
||||||
const tools = ref([
|
const { t, locale } = useI18n()
|
||||||
{
|
const route = useRoute()
|
||||||
path: '/json-formatter',
|
|
||||||
title: 'JSON',
|
const currentPathLocale = computed(() => route.params.locale || 'zh')
|
||||||
description: '格式化、验证和美化JSON数据'
|
|
||||||
},
|
const heroDate = computed(() => {
|
||||||
{
|
const d = new Date()
|
||||||
path: '/comparator',
|
const y = d.getFullYear()
|
||||||
title: '对比',
|
const m = d.getMonth() + 1
|
||||||
description: '文本和JSON对比工具'
|
const day = d.getDate()
|
||||||
},
|
if (locale.value === 'en') {
|
||||||
{
|
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
|
||||||
path: '/encoder-decoder',
|
|
||||||
title: '编解码',
|
|
||||||
description: '编码/解码工具'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/variable-name',
|
|
||||||
title: '变量名',
|
|
||||||
description: '变量名格式转换'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/qr-code',
|
|
||||||
title: '二维码',
|
|
||||||
description: '生成二维码'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/timestamp-converter',
|
|
||||||
title: '时间戳',
|
|
||||||
description: '时间戳与时间字符串相互转换'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/color-converter',
|
|
||||||
title: '颜色',
|
|
||||||
description: '颜色格式转换'
|
|
||||||
}
|
}
|
||||||
|
return `${y}年${m}月${day}日`
|
||||||
|
})
|
||||||
|
|
||||||
|
const tools = computed(() => [
|
||||||
|
{ path: 'json-formatter', title: t('home.toolJson'), description: t('home.toolJsonDesc') },
|
||||||
|
{ path: 'comparator', title: t('home.toolComparator'), description: t('home.toolComparatorDesc') },
|
||||||
|
{ path: 'encoder-decoder', title: t('home.toolEncoderDecoder'), description: t('home.toolEncoderDecoderDesc') },
|
||||||
|
{ path: 'variable-name', title: t('home.toolVariableName'), description: t('home.toolVariableNameDesc') },
|
||||||
|
{ path: 'qr-code', title: t('home.toolQrCode'), description: t('home.toolQrCodeDesc') },
|
||||||
|
{ path: 'timestamp-converter', title: t('home.toolTimestamp'), description: t('home.toolTimestampDesc') },
|
||||||
|
{ path: 'color-converter', title: t('home.toolColor'), description: t('home.toolColorDesc') },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const words_script = document.createElement('script')
|
||||||
|
words_script.charset = 'utf-8'
|
||||||
|
words_script.src = 'https://sdk.jinrishici.com/v2/browser/jinrishici.js'
|
||||||
|
document.body.appendChild(words_script)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -167,9 +167,6 @@ const tools = ref([
|
|||||||
.tool-card {
|
.tool-card {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.tool-card {
|
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<i v-else class="fas fa-circle-info"></i>
|
<i v-else class="fas fa-circle-info"></i>
|
||||||
<span>{{ toastMessage }}</span>
|
<span>{{ toastMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="closeToast" class="toast-close-btn" title="关闭">
|
<button @click="closeToast" class="toast-close-btn" :title="t('common.close')">
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,12 +17,12 @@
|
|||||||
<!-- 左侧侧栏(历史记录) -->
|
<!-- 左侧侧栏(历史记录) -->
|
||||||
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h3>历史记录</h3>
|
<h3>{{ t('common.history') }}</h3>
|
||||||
<button @click="toggleSidebar" class="close-btn">×</button>
|
<button @click="toggleSidebar" class="close-btn">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<div v-if="historyList.length === 0" class="empty-history">
|
<div v-if="historyList.length === 0" class="empty-history">
|
||||||
暂无历史记录
|
{{ t('common.noHistory') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in historyList"
|
v-for="(item, index) in historyList"
|
||||||
@@ -42,28 +42,28 @@
|
|||||||
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
|
<div class="left-panel" :style="{ width: leftPanelWidth + '%' }">
|
||||||
<div class="panel-toolbar">
|
<div class="panel-toolbar">
|
||||||
<div class="view-tabs">
|
<div class="view-tabs">
|
||||||
<button class="view-tab active">编辑器</button>
|
<button class="view-tab active">{{ t('json.editor') }} <span class="size-limit">{{ t('json.maxSize') }}</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<button @click="copyToClipboard" class="toolbar-icon-btn" title="复制">
|
<button @click="copyToClipboard" class="toolbar-icon-btn" :title="t('common.copy')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="pasteFromClipboard" class="toolbar-icon-btn" title="粘贴">
|
<button @click="pasteFromClipboard" class="toolbar-icon-btn" :title="t('common.paste')">
|
||||||
<i class="far fa-paste"></i>
|
<i class="far fa-paste"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="clearAll" class="toolbar-icon-btn" title="清空">
|
<button @click="clearAll" class="toolbar-icon-btn" :title="t('common.clear')">
|
||||||
<i class="far fa-trash-can"></i>
|
<i class="far fa-trash-can"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="formatJson" class="toolbar-icon-btn" title="格式化">
|
<button @click="formatJson" class="toolbar-icon-btn" :title="t('json.format')">
|
||||||
<i class="fas fa-align-left"></i>
|
<i class="fas fa-align-left"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="minifyJson" class="toolbar-icon-btn" title="压缩">
|
<button @click="minifyJson" class="toolbar-icon-btn" :title="t('json.minify')">
|
||||||
<i class="fas fa-down-left-and-up-right-to-center"></i>
|
<i class="fas fa-down-left-and-up-right-to-center"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="escapeJson" class="toolbar-icon-btn" title="转义">
|
<button @click="escapeJson" class="toolbar-icon-btn" :title="t('json.escape')">
|
||||||
<i class="fas fa-code"></i>
|
<i class="fas fa-code"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="unescapeJson" class="toolbar-icon-btn" title="取消转义">
|
<button @click="unescapeJson" class="toolbar-icon-btn" :title="t('json.unescape')">
|
||||||
<svg viewBox="150 -50 1100 1250" xmlns="http://www.w3.org/2000/svg" width="17" height="17"><path d="M285.352637 0.003641h111.956538v114.687184h-111.956538v282.621991a110.682235 110.682235 0 0 1-33.313896 81.282266 110.682235 110.682235 0 0 1-81.282266 33.313896 110.682235 110.682235 0 0 1 81.282266 33.313897 110.682235 110.682235 0 0 1 33.313896 81.282266v282.621991h111.956538v114.687184h-111.956538a188.050574 188.050574 0 0 1-80.007964-40.049493 93.570179 93.570179 0 0 1-34.67922-74.637691v-226.643722a110.682235 110.682235 0 0 0-33.313896-81.282267 110.682235 110.682235 0 0 0-81.282267-33.313896H0v-111.956537h55.978269a110.682235 110.682235 0 0 0 81.282266-33.313897 110.682235 110.682235 0 0 0 33.313896-81.282266V114.690825A113.776969 113.776969 0 0 1 285.261616 0.003641z m794.61835 0a113.776969 113.776969 0 0 1 114.687184 114.687184v226.643722a113.776969 113.776969 0 0 0 114.687185 114.687184H1365.323624v111.956538h-55.978268a113.776969 113.776969 0 0 0-114.687185 114.687184v226.643722a113.776969 113.776969 0 0 1-114.687184 114.687184h-111.956537V909.309175h111.956537V626.687184a113.776969 113.776969 0 0 1 114.687184-114.687184 113.776969 113.776969 0 0 1-114.687184-114.687184V114.690825h-111.956537V0.003641h111.956537zM682.661812 682.665453a54.612945 54.612945 0 0 1 55.978269 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 54.612945 54.612945 0 0 1-80.007965 0 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z m-226.643721 0a54.612945 54.612945 0 0 1 55.978268 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 52.246384 52.246384 0 0 1-40.049493 17.294099 59.164024 59.164024 0 0 1-58.708916-58.708916 52.246384 52.246384 0 0 1 17.294099-40.049493 58.799937 58.799937 0 0 1 41.505839-15.837754z m453.287443 0a58.799937 58.799937 0 0 1 41.323795 16.019797 52.246384 52.246384 0 0 1 17.294099 40.049493 59.164024 59.164024 0 0 1-58.708916 58.708916 52.246384 52.246384 0 0 1-40.049493-17.294099 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z" fill="#666666" p-id="26339"></path></svg>
|
<svg viewBox="150 -50 1100 1250" xmlns="http://www.w3.org/2000/svg" width="17" height="17"><path d="M285.352637 0.003641h111.956538v114.687184h-111.956538v282.621991a110.682235 110.682235 0 0 1-33.313896 81.282266 110.682235 110.682235 0 0 1-81.282266 33.313896 110.682235 110.682235 0 0 1 81.282266 33.313897 110.682235 110.682235 0 0 1 33.313896 81.282266v282.621991h111.956538v114.687184h-111.956538a188.050574 188.050574 0 0 1-80.007964-40.049493 93.570179 93.570179 0 0 1-34.67922-74.637691v-226.643722a110.682235 110.682235 0 0 0-33.313896-81.282267 110.682235 110.682235 0 0 0-81.282267-33.313896H0v-111.956537h55.978269a110.682235 110.682235 0 0 0 81.282266-33.313897 110.682235 110.682235 0 0 0 33.313896-81.282266V114.690825A113.776969 113.776969 0 0 1 285.261616 0.003641z m794.61835 0a113.776969 113.776969 0 0 1 114.687184 114.687184v226.643722a113.776969 113.776969 0 0 0 114.687185 114.687184H1365.323624v111.956538h-55.978268a113.776969 113.776969 0 0 0-114.687185 114.687184v226.643722a113.776969 113.776969 0 0 1-114.687184 114.687184h-111.956537V909.309175h111.956537V626.687184a113.776969 113.776969 0 0 1 114.687184-114.687184 113.776969 113.776969 0 0 1-114.687184-114.687184V114.690825h-111.956537V0.003641h111.956537zM682.661812 682.665453a54.612945 54.612945 0 0 1 55.978269 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 54.612945 54.612945 0 0 1-80.007965 0 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z m-226.643721 0a54.612945 54.612945 0 0 1 55.978268 55.978269 58.799937 58.799937 0 0 1-16.019797 41.323795 52.246384 52.246384 0 0 1-40.049493 17.294099 59.164024 59.164024 0 0 1-58.708916-58.708916 52.246384 52.246384 0 0 1 17.294099-40.049493 58.799937 58.799937 0 0 1 41.505839-15.837754z m453.287443 0a58.799937 58.799937 0 0 1 41.323795 16.019797 52.246384 52.246384 0 0 1 17.294099 40.049493 59.164024 59.164024 0 0 1-58.708916 58.708916 52.246384 52.246384 0 0 1-40.049493-17.294099 58.799937 58.799937 0 0 1-16.019797-41.323795 54.612945 54.612945 0 0 1 55.978269-55.978269z" fill="#666666" p-id="26339"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
@paste="handlePaste"
|
@paste="handlePaste"
|
||||||
@input="updateLineCount"
|
@input="updateLineCount"
|
||||||
@focus="adjustTextareaHeight"
|
@focus="adjustTextareaHeight"
|
||||||
placeholder='请输入或粘贴JSON数据,例如:{"name":"工具箱","version":1.0}'
|
:placeholder="t('json.placeholder')"
|
||||||
class="json-editor"
|
class="json-editor"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<div class="right-panel" :style="{ width: rightPanelWidth + '%' }">
|
<div class="right-panel" :style="{ width: rightPanelWidth + '%' }">
|
||||||
<div class="panel-toolbar">
|
<div class="panel-toolbar">
|
||||||
<div class="view-tabs">
|
<div class="view-tabs">
|
||||||
<button class="view-tab active">树形</button>
|
<button class="view-tab active">{{ t('json.tree') }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-actions">
|
<div class="toolbar-actions">
|
||||||
<div class="jsonpath-input-wrapper">
|
<div class="jsonpath-input-wrapper">
|
||||||
@@ -109,15 +109,15 @@
|
|||||||
@focus="showJsonPathHistory = true"
|
@focus="showJsonPathHistory = true"
|
||||||
@blur="handleJsonPathBlur"
|
@blur="handleJsonPathBlur"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="输入 JSONPath,例如: $.key.subkey"
|
:placeholder="t('json.jsonPathPlaceholder')"
|
||||||
class="jsonpath-input"
|
class="jsonpath-input"
|
||||||
title="JSONPath 筛选"
|
:title="t('json.jsonPathFilter')"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="jsonPathQuery"
|
v-if="jsonPathQuery"
|
||||||
@click="clearJsonPath"
|
@click="clearJsonPath"
|
||||||
class="jsonpath-clear-btn"
|
class="jsonpath-clear-btn"
|
||||||
title="清除筛选"
|
:title="t('json.clearFilter')"
|
||||||
>
|
>
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -140,14 +140,14 @@
|
|||||||
v-if="jsonPathQuery && jsonPathQuery.trim() && matchedNodes.length > 0"
|
v-if="jsonPathQuery && jsonPathQuery.trim() && matchedNodes.length > 0"
|
||||||
@click="copyMatchedResults"
|
@click="copyMatchedResults"
|
||||||
class="toolbar-icon-btn"
|
class="toolbar-icon-btn"
|
||||||
title="复制筛选结果"
|
:title="t('json.copyFilterResult')"
|
||||||
>
|
>
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="expandAll" class="toolbar-icon-btn" title="展开全部">
|
<button @click="expandAll" class="toolbar-icon-btn" :title="t('json.expandAll')">
|
||||||
<i class="fas fa-chevron-down"></i>
|
<i class="fas fa-chevron-down"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="collapseAll" class="toolbar-icon-btn" title="折叠全部">
|
<button @click="collapseAll" class="toolbar-icon-btn" :title="t('json.collapseAll')">
|
||||||
<i class="fas fa-chevron-up"></i>
|
<i class="fas fa-chevron-up"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
<div class="tree-container">
|
<div class="tree-container">
|
||||||
<div class="tree-content">
|
<div class="tree-content">
|
||||||
<div v-if="!parsedData" class="empty-state">
|
<div v-if="!parsedData" class="empty-state">
|
||||||
在左侧输入或粘贴JSON数据,右侧将实时显示树形结构
|
{{ t('json.emptyState') }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 有 JSONPath 筛选时,直接显示匹配的节点列表 -->
|
<!-- 有 JSONPath 筛选时,直接显示匹配的节点列表 -->
|
||||||
<div v-else-if="jsonPathQuery && jsonPathQuery.trim() && matchedNodes.length > 0" class="matched-nodes-list">
|
<div v-else-if="jsonPathQuery && jsonPathQuery.trim() && matchedNodes.length > 0" class="matched-nodes-list">
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="jsonPathQuery && jsonPathQuery.trim() && matchedNodes.length === 0" class="empty-state">
|
<div v-else-if="jsonPathQuery && jsonPathQuery.trim() && matchedNodes.length === 0" class="empty-state">
|
||||||
未找到匹配的节点
|
{{ t('json.noMatchedNodes') }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 没有筛选时,显示完整的树形结构 -->
|
<!-- 没有筛选时,显示完整的树形结构 -->
|
||||||
<JsonTreeNode
|
<JsonTreeNode
|
||||||
@@ -197,8 +197,18 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import JsonTreeNode from '../components/JsonTreeNode.vue'
|
import JsonTreeNode from '../components/JsonTreeNode.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 最大输入限制:5MB(JSON格式化工具的限制)
|
||||||
|
// 主要考虑因素:
|
||||||
|
// 1. JSON.parse 可以处理更大的 JSON(10-50MB)
|
||||||
|
// 2. DOM 渲染是主要瓶颈:大型 JSON 会创建大量 DOM 节点
|
||||||
|
// 3. 路径遍历和展开/折叠状态管理也会消耗内存
|
||||||
|
const MAX_INPUT_BYTES = 5 * 1024 * 1024 // 5MB
|
||||||
|
|
||||||
const inputJson = ref('')
|
const inputJson = ref('')
|
||||||
const sidebarOpen = ref(false)
|
const sidebarOpen = ref(false)
|
||||||
const leftPanelWidth = ref(50)
|
const leftPanelWidth = ref(50)
|
||||||
@@ -602,7 +612,7 @@ const handleJsonPathInput = () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
matchedPaths.value.clear()
|
matchedPaths.value.clear()
|
||||||
console.error('JSONPath 解析错误:', e)
|
// JSONPath 解析错误,忽略
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +641,7 @@ const loadJsonPathHistory = () => {
|
|||||||
jsonPathHistory.value = JSON.parse(stored)
|
jsonPathHistory.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载 JSONPath 历史记录失败', e)
|
// 加载 JSONPath 历史记录失败,重置为空数组
|
||||||
jsonPathHistory.value = []
|
jsonPathHistory.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -660,7 +670,7 @@ const saveJsonPathHistory = (jsonPath) => {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(JSONPATH_HISTORY_KEY, JSON.stringify(jsonPathHistory.value))
|
localStorage.setItem(JSONPATH_HISTORY_KEY, JSON.stringify(jsonPathHistory.value))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存 JSONPath 历史记录失败', e)
|
// 保存 JSONPath 历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,7 +692,7 @@ const handleJsonPathBlur = () => {
|
|||||||
// 复制筛选结果
|
// 复制筛选结果
|
||||||
const copyMatchedResults = async () => {
|
const copyMatchedResults = async () => {
|
||||||
if (!matchedNodes.value || matchedNodes.value.length === 0) {
|
if (!matchedNodes.value || matchedNodes.value.length === 0) {
|
||||||
showToast('没有可复制的结果', 'error', 2000)
|
showToast(t('json.noContentToCopy'), 'error', 2000)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -693,14 +703,34 @@ const copyMatchedResults = async () => {
|
|||||||
|
|
||||||
// 复制到剪贴板
|
// 复制到剪贴板
|
||||||
await navigator.clipboard.writeText(jsonString)
|
await navigator.clipboard.writeText(jsonString)
|
||||||
showToast(`已复制 ${matchedNodes.value.length} 个匹配结果`, 'info', 2000)
|
showToast(t('json.copiedCount', { count: matchedNodes.value.length }), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('复制失败:' + e.message, 'error', 3000)
|
showToast(t('common.copyFailed') + e.message, 'error', 3000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取字符串 UTF-8 字节长度
|
||||||
|
const getByteLength = (str) => new TextEncoder().encode(str).length
|
||||||
|
|
||||||
|
// 将字符串截断到最大字节数(避免切断多字节字符)
|
||||||
|
const truncateToMaxBytes = (str, maxBytes) => {
|
||||||
|
if (getByteLength(str) <= maxBytes) return str
|
||||||
|
let end = str.length
|
||||||
|
while (end > 0 && getByteLength(str.slice(0, end)) > maxBytes) end--
|
||||||
|
return str.slice(0, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用输入大小限制,超出则截断并提示
|
||||||
|
const applyInputLimit = () => {
|
||||||
|
if (getByteLength(inputJson.value) <= MAX_INPUT_BYTES) return
|
||||||
|
inputJson.value = truncateToMaxBytes(inputJson.value, MAX_INPUT_BYTES)
|
||||||
|
showToast(t('json.contentOverLimit'), 'info', 3000)
|
||||||
|
updateLineCount()
|
||||||
|
}
|
||||||
|
|
||||||
// 更新行号
|
// 更新行号
|
||||||
const updateLineCount = () => {
|
const updateLineCount = () => {
|
||||||
|
applyInputLimit()
|
||||||
if (inputJson.value) {
|
if (inputJson.value) {
|
||||||
lineCount.value = inputJson.value.split('\n').length
|
lineCount.value = inputJson.value.split('\n').length
|
||||||
} else {
|
} else {
|
||||||
@@ -849,12 +879,21 @@ const getAllPaths = (obj, prefix = 'root') => {
|
|||||||
|
|
||||||
// 监听输入变化,实时更新树形结构
|
// 监听输入变化,实时更新树形结构
|
||||||
watch(inputJson, () => {
|
watch(inputJson, () => {
|
||||||
|
// 先应用大小限制
|
||||||
|
applyInputLimit()
|
||||||
|
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
// 使用nextTick确保DOM更新后再调整高度
|
// 使用nextTick确保DOM更新后再调整高度
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
adjustTextareaHeight()
|
adjustTextareaHeight()
|
||||||
}, 0)
|
}, 0)
|
||||||
if (inputJson.value.trim()) {
|
if (inputJson.value.trim()) {
|
||||||
|
// 检查大小,如果超过限制则不解析(避免性能问题)
|
||||||
|
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
||||||
|
treeLineCount.value = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(inputJson.value)
|
const parsed = JSON.parse(inputJson.value)
|
||||||
expandedNodes.value.clear()
|
expandedNodes.value.clear()
|
||||||
@@ -876,43 +915,80 @@ watch(inputJson, () => {
|
|||||||
// 格式化JSON
|
// 格式化JSON
|
||||||
const formatJson = () => {
|
const formatJson = () => {
|
||||||
if (!inputJson.value.trim()) {
|
if (!inputJson.value.trim()) {
|
||||||
showToast('请输入JSON数据')
|
showToast(t('json.pleaseInputJson'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查输入大小
|
||||||
|
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.inputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(inputJson.value)
|
const parsed = JSON.parse(inputJson.value)
|
||||||
inputJson.value = JSON.stringify(parsed, null, 2)
|
const formatted = JSON.stringify(parsed, null, 2)
|
||||||
|
|
||||||
|
// 检查格式化后的大小
|
||||||
|
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.outputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputJson.value = formatted
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('格式化成功', 'info', 2000)
|
showToast(t('json.formatSuccess'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('JSON格式错误:' + e.message)
|
showToast(t('json.jsonError') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 压缩JSON
|
// 压缩JSON
|
||||||
const minifyJson = () => {
|
const minifyJson = () => {
|
||||||
if (!inputJson.value.trim()) {
|
if (!inputJson.value.trim()) {
|
||||||
showToast('请输入JSON数据')
|
showToast(t('json.pleaseInputJson'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查输入大小
|
||||||
|
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.minifyOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(inputJson.value)
|
const parsed = JSON.parse(inputJson.value)
|
||||||
inputJson.value = JSON.stringify(parsed)
|
const minified = JSON.stringify(parsed)
|
||||||
|
|
||||||
|
// 检查压缩后的大小(压缩后应该更小,但为了安全还是检查)
|
||||||
|
if (getByteLength(minified) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.minifyOutputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputJson.value = minified
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('压缩成功', 'info', 2000)
|
showToast(t('json.minifySuccess'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('JSON格式错误:' + e.message)
|
showToast(t('json.jsonError') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转义JSON
|
// 转义JSON
|
||||||
const escapeJson = () => {
|
const escapeJson = () => {
|
||||||
if (!inputJson.value.trim()) {
|
if (!inputJson.value.trim()) {
|
||||||
showToast('请输入JSON数据')
|
showToast(t('json.pleaseInputJson'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查输入大小
|
||||||
|
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.escapeOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let jsonToEscape = inputJson.value.trim()
|
let jsonToEscape = inputJson.value.trim()
|
||||||
|
|
||||||
@@ -966,24 +1042,43 @@ const escapeJson = () => {
|
|||||||
// 不是JSON,保持原样
|
// 不是JSON,保持原样
|
||||||
}
|
}
|
||||||
// 添加引号并转义
|
// 添加引号并转义
|
||||||
inputJson.value = JSON.stringify(jsonToEscape)
|
const escaped = JSON.stringify(jsonToEscape)
|
||||||
|
// 检查转义后的大小
|
||||||
|
if (getByteLength(escaped) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.escapeOutputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inputJson.value = escaped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 最后检查一次大小(防止前面的分支没有检查)
|
||||||
|
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.escapeOutputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('转义成功', 'info', 2000)
|
showToast(t('json.escapeSuccess'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('转义失败:' + e.message)
|
showToast(t('json.escapeFailed') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消转义JSON
|
// 取消转义JSON
|
||||||
const unescapeJson = () => {
|
const unescapeJson = () => {
|
||||||
if (!inputJson.value.trim()) {
|
if (!inputJson.value.trim()) {
|
||||||
showToast('请输入JSON数据')
|
showToast(t('json.pleaseInputJson'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查输入大小
|
||||||
|
if (getByteLength(inputJson.value) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.unescapeOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let jsonToParse = inputJson.value.trim()
|
let jsonToParse = inputJson.value.trim()
|
||||||
|
|
||||||
@@ -1027,50 +1122,73 @@ const unescapeJson = () => {
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(unescaped)
|
const parsed = JSON.parse(unescaped)
|
||||||
// 如果解析成功,自动格式化
|
// 如果解析成功,自动格式化
|
||||||
inputJson.value = JSON.stringify(parsed, null, 2)
|
const formatted = JSON.stringify(parsed, null, 2)
|
||||||
|
// 检查格式化后的大小
|
||||||
|
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.unescapeOutputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inputJson.value = formatted
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('取消转义并格式化成功', 'info', 2000)
|
showToast(t('json.unescapeFormatSuccess'), 'info', 2000)
|
||||||
return
|
return
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果解析失败,说明只是普通字符串,保持原样
|
// 如果解析失败,说明只是普通字符串,保持原样
|
||||||
|
// 检查字符串大小
|
||||||
|
if (getByteLength(unescaped) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.unescapeOutputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
inputJson.value = unescaped
|
inputJson.value = unescaped
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('取消转义成功', 'info', 2000)
|
showToast(t('json.unescapeSuccess'), 'info', 2000)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果取消转义后是对象或数组,自动格式化
|
// 如果取消转义后是对象或数组,自动格式化
|
||||||
if (typeof unescaped === 'object' && unescaped !== null) {
|
if (typeof unescaped === 'object' && unescaped !== null) {
|
||||||
inputJson.value = JSON.stringify(unescaped, null, 2)
|
const formatted = JSON.stringify(unescaped, null, 2)
|
||||||
|
// 检查格式化后的大小
|
||||||
|
if (getByteLength(formatted) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.unescapeOutputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inputJson.value = formatted
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('取消转义并格式化成功', 'info', 2000)
|
showToast(t('json.unescapeFormatSuccess'), 'info', 2000)
|
||||||
} else {
|
} else {
|
||||||
// 其他类型(数字、布尔值等),转换为字符串
|
// 其他类型(数字、布尔值等),转换为字符串
|
||||||
inputJson.value = String(unescaped)
|
const result = String(unescaped)
|
||||||
|
// 检查结果大小
|
||||||
|
if (getByteLength(result) > MAX_INPUT_BYTES) {
|
||||||
|
showToast(t('json.unescapeOutputOverLimit'), 'error', 4000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inputJson.value = result
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
resetEditorScroll()
|
resetEditorScroll()
|
||||||
showToast('取消转义成功', 'info', 2000)
|
showToast(t('json.unescapeSuccess'), 'info', 2000)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('取消转义失败:' + e.message)
|
showToast(t('json.unescapeFailed') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制到剪贴板
|
// 复制到剪贴板
|
||||||
const copyToClipboard = async () => {
|
const copyToClipboard = async () => {
|
||||||
if (!inputJson.value.trim()) {
|
if (!inputJson.value.trim()) {
|
||||||
showToast('编辑器内容为空,无法复制')
|
showToast(t('json.editorEmpty'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(inputJson.value)
|
await navigator.clipboard.writeText(inputJson.value)
|
||||||
showToast('已复制到剪贴板', 'info', 2000)
|
showToast(t('common.copied'), 'info', 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('复制失败:' + e.message)
|
showToast(t('common.copyFailed') + e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,8 +1197,13 @@ const pasteFromClipboard = async () => {
|
|||||||
// 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
// 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
|
||||||
if (navigator.clipboard && navigator.clipboard.readText) {
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
||||||
try {
|
try {
|
||||||
const text = await navigator.clipboard.readText()
|
let text = await navigator.clipboard.readText()
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
|
// 检查大小限制
|
||||||
|
if (getByteLength(text) > MAX_INPUT_BYTES) {
|
||||||
|
text = truncateToMaxBytes(text, MAX_INPUT_BYTES)
|
||||||
|
showToast(t('json.pasteOverLimit'), 'info', 3000)
|
||||||
|
}
|
||||||
inputJson.value = text
|
inputJson.value = text
|
||||||
updateLineCount()
|
updateLineCount()
|
||||||
// 粘贴后不重置滚动位置,保持在当前位置
|
// 粘贴后不重置滚动位置,保持在当前位置
|
||||||
@@ -1096,7 +1219,7 @@ const pasteFromClipboard = async () => {
|
|||||||
// 如果不是有效JSON,不保存到历史记录
|
// 如果不是有效JSON,不保存到历史记录
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
showToast('剪贴板内容为空')
|
showToast(t('common.clipboardEmpty'))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1108,9 +1231,9 @@ const pasteFromClipboard = async () => {
|
|||||||
// 让现有的 handlePaste 方法处理粘贴逻辑
|
// 让现有的 handlePaste 方法处理粘贴逻辑
|
||||||
if (jsonEditorRef.value) {
|
if (jsonEditorRef.value) {
|
||||||
jsonEditorRef.value.focus()
|
jsonEditorRef.value.focus()
|
||||||
showToast('请按 Ctrl+V 或 Cmd+V 粘贴内容', 'info', 3000)
|
showToast(t('common.pasteHint'), 'info', 3000)
|
||||||
} else {
|
} else {
|
||||||
showToast('无法访问编辑器,请手动粘贴内容')
|
showToast(t('encoder.manualPaste'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1120,7 +1243,7 @@ const clearAll = () => {
|
|||||||
expandedNodes.value.clear()
|
expandedNodes.value.clear()
|
||||||
lineCount.value = 1
|
lineCount.value = 1
|
||||||
treeLineCount.value = 1
|
treeLineCount.value = 1
|
||||||
showToast('已清空', 'info', 2000)
|
showToast(t('common.cleared'), 'info', 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理粘贴事件
|
// 处理粘贴事件
|
||||||
@@ -1129,6 +1252,16 @@ const handlePaste = async (event) => {
|
|||||||
const pastedText = event.clipboardData?.getData('text') || ''
|
const pastedText = event.clipboardData?.getData('text') || ''
|
||||||
|
|
||||||
if (pastedText.trim()) {
|
if (pastedText.trim()) {
|
||||||
|
// 检查大小限制
|
||||||
|
if (getByteLength(pastedText) > MAX_INPUT_BYTES) {
|
||||||
|
event.preventDefault()
|
||||||
|
const truncated = truncateToMaxBytes(pastedText, MAX_INPUT_BYTES)
|
||||||
|
inputJson.value = truncated
|
||||||
|
showToast(t('json.pasteOverLimit'), 'info', 3000)
|
||||||
|
updateLineCount()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 等待下一个tick,确保inputJson已更新
|
// 等待下一个tick,确保inputJson已更新
|
||||||
await new Promise(resolve => setTimeout(resolve, 0))
|
await new Promise(resolve => setTimeout(resolve, 0))
|
||||||
|
|
||||||
@@ -1163,7 +1296,7 @@ const saveToHistory = (json) => {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('读取历史记录失败', e)
|
// 读取历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到开头
|
// 添加到开头
|
||||||
@@ -1179,7 +1312,7 @@ const saveToHistory = (json) => {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存历史记录失败', e)
|
// 保存历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,7 +1324,7 @@ const loadHistoryList = () => {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载历史记录失败', e)
|
// 加载历史记录失败,重置为空数组
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1637,6 +1770,13 @@ onUnmounted(() => {
|
|||||||
border-bottom-color: #1a1a1a;
|
border-bottom-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-tab .size-limit {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-actions {
|
.toolbar-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<i v-else class="fas fa-circle-check"></i>
|
<i v-else class="fas fa-circle-check"></i>
|
||||||
<span>{{ toastMessage }}</span>
|
<span>{{ toastMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="closeToast" class="toast-close-btn" title="关闭">
|
<button @click="closeToast" class="toast-close-btn" :title="t('common.close')">
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,12 +18,12 @@
|
|||||||
<!-- 左侧侧栏(历史记录) -->
|
<!-- 左侧侧栏(历史记录) -->
|
||||||
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
<div class="sidebar" :class="{ 'sidebar-open': sidebarOpen }">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h3>历史记录</h3>
|
<h3>{{ t('common.history') }}</h3>
|
||||||
<button @click="toggleSidebar" class="close-btn">×</button>
|
<button @click="toggleSidebar" class="close-btn">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<div v-if="historyList.length === 0" class="empty-history">
|
<div v-if="historyList.length === 0" class="empty-history">
|
||||||
暂无历史记录
|
{{ t('common.noHistory') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in historyList"
|
v-for="(item, index) in historyList"
|
||||||
@@ -47,30 +47,30 @@
|
|||||||
<textarea
|
<textarea
|
||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
@keydown.enter.prevent="generateQRCode"
|
@keydown.enter.prevent="generateQRCode"
|
||||||
placeholder="请输入要生成二维码的内容"
|
:placeholder="t('qr.inputPlaceholder')"
|
||||||
class="input-textarea"
|
class="input-textarea"
|
||||||
rows="4"
|
rows="4"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button @click="generateQRCode" class="generate-btn">
|
<button @click="generateQRCode" class="generate-btn">
|
||||||
<i class="fas fa-qrcode"></i>
|
<i class="fas fa-qrcode"></i>
|
||||||
生成二维码
|
{{ t('qr.generate') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 二维码显示区域 -->
|
<!-- 二维码显示区域 -->
|
||||||
<div v-if="qrCodeDataUrl" class="qr-display-section">
|
<div v-if="qrCodeDataUrl" class="qr-display-section">
|
||||||
<div class="qr-code-wrapper">
|
<div class="qr-code-wrapper">
|
||||||
<img :src="qrCodeDataUrl" alt="二维码" class="qr-code-image" />
|
<img :src="qrCodeDataUrl" :alt="t('qr.qrCode')" class="qr-code-image" />
|
||||||
</div>
|
</div>
|
||||||
<div class="qr-actions">
|
<div class="qr-actions">
|
||||||
<button @click="downloadQRCode" class="action-btn">
|
<button @click="downloadQRCode" class="action-btn">
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
下载
|
{{ t('qr.download') }}
|
||||||
</button>
|
</button>
|
||||||
<button @click="copyQRCodeImage" class="action-btn">
|
<button @click="copyQRCodeImage" class="action-btn">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
复制图片
|
{{ t('qr.copyImage') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,8 +90,11 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
// 输入文本
|
// 输入文本
|
||||||
const inputText = ref('')
|
const inputText = ref('')
|
||||||
// 二维码数据URL
|
// 二维码数据URL
|
||||||
@@ -135,7 +138,7 @@ const closeToast = () => {
|
|||||||
// 生成二维码
|
// 生成二维码
|
||||||
const generateQRCode = async () => {
|
const generateQRCode = async () => {
|
||||||
if (!inputText.value.trim()) {
|
if (!inputText.value.trim()) {
|
||||||
showToast('请输入要生成二维码的内容', 'error')
|
showToast(t('qr.pleaseInput'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +158,9 @@ const generateQRCode = async () => {
|
|||||||
// 保存到历史记录
|
// 保存到历史记录
|
||||||
saveToHistory(inputText.value.trim())
|
saveToHistory(inputText.value.trim())
|
||||||
|
|
||||||
showToast('二维码生成成功', 'success', 2000)
|
showToast(t('qr.generateSuccess'), 'success', 2000)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('生成二维码失败:' + error.message, 'error')
|
showToast(t('qr.generateFailed') + error.message, 'error')
|
||||||
qrCodeDataUrl.value = ''
|
qrCodeDataUrl.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +168,7 @@ const generateQRCode = async () => {
|
|||||||
// 下载二维码
|
// 下载二维码
|
||||||
const downloadQRCode = () => {
|
const downloadQRCode = () => {
|
||||||
if (!qrCodeDataUrl.value) {
|
if (!qrCodeDataUrl.value) {
|
||||||
showToast('没有可下载的二维码', 'error')
|
showToast(t('qr.noQrToDownload'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,16 +177,16 @@ const downloadQRCode = () => {
|
|||||||
link.download = `qrcode-${Date.now()}.png`
|
link.download = `qrcode-${Date.now()}.png`
|
||||||
link.href = qrCodeDataUrl.value
|
link.href = qrCodeDataUrl.value
|
||||||
link.click()
|
link.click()
|
||||||
showToast('下载成功', 'success', 2000)
|
showToast(t('qr.downloadSuccess'), 'success', 2000)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('下载失败:' + error.message, 'error')
|
showToast(t('qr.downloadFailed') + error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制二维码图片
|
// 复制二维码图片
|
||||||
const copyQRCodeImage = async () => {
|
const copyQRCodeImage = async () => {
|
||||||
if (!qrCodeDataUrl.value) {
|
if (!qrCodeDataUrl.value) {
|
||||||
showToast('没有可复制的二维码', 'error')
|
showToast(t('qr.noQrToCopy'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,10 +202,9 @@ const copyQRCodeImage = async () => {
|
|||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
showToast('已复制到剪贴板', 'success', 2000)
|
showToast(t('common.copied'), 'success', 2000)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 降级方案:提示用户手动保存
|
showToast(t('qr.copyImageFailed'), 'error')
|
||||||
showToast('复制失败,请使用下载功能', 'error')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +223,7 @@ const saveToHistory = (text) => {
|
|||||||
history = JSON.parse(stored)
|
history = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('读取历史记录失败', e)
|
// 读取历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复保存相同的记录
|
// 避免重复保存相同的记录
|
||||||
@@ -243,7 +245,7 @@ const saveToHistory = (text) => {
|
|||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history))
|
||||||
loadHistoryList()
|
loadHistoryList()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存历史记录失败', e)
|
// 保存历史记录失败,忽略错误
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +257,7 @@ const loadHistoryList = () => {
|
|||||||
historyList.value = JSON.parse(stored)
|
historyList.value = JSON.parse(stored)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载历史记录失败', e)
|
// 加载历史记录失败,重置为空数组
|
||||||
historyList.value = []
|
historyList.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<!-- 日期转换为时间戳 -->
|
<!-- 日期转换为时间戳 -->
|
||||||
<div class="conversion-row">
|
<div class="conversion-row">
|
||||||
<div class="conversion-label">日期 → ({{ timezoneLabel }}) 时间戳:</div>
|
<div class="conversion-label">{{ t('timestamp.dateToTs', { tz: timezoneLabel }) }}</div>
|
||||||
<div class="conversion-inputs">
|
<div class="conversion-inputs">
|
||||||
<div class="input-with-calendar">
|
<div class="input-with-calendar">
|
||||||
<input
|
<input
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
:placeholder="getDatePlaceholder()"
|
:placeholder="getDatePlaceholder()"
|
||||||
class="input-field"
|
class="input-field"
|
||||||
/>
|
/>
|
||||||
<button @click="showDateTimePicker = true" class="calendar-btn" title="选择日期时间">
|
<button @click="showDateTimePicker = true" class="calendar-btn" :title="t('timestamp.selectDateTime')">
|
||||||
<i class="far fa-calendar"></i>
|
<i class="far fa-calendar"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
readonly
|
readonly
|
||||||
class="input-field readonly"
|
class="input-field readonly"
|
||||||
/>
|
/>
|
||||||
<button @click="copyToClipboard(timestampOutput)" class="copy-btn" title="复制">
|
<button @click="copyToClipboard(timestampOutput)" class="copy-btn" :title="t('common.copy')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,13 +62,13 @@
|
|||||||
|
|
||||||
<!-- 时间戳转换为日期 -->
|
<!-- 时间戳转换为日期 -->
|
||||||
<div class="conversion-row">
|
<div class="conversion-row">
|
||||||
<div class="conversion-label">时间戳 → ({{ timezoneLabel }}) 日期</div>
|
<div class="conversion-label">{{ t('timestamp.tsToDate', { tz: timezoneLabel }) }}</div>
|
||||||
<div class="conversion-inputs">
|
<div class="conversion-inputs">
|
||||||
<input
|
<input
|
||||||
v-model="timestampInput"
|
v-model="timestampInput"
|
||||||
@input="convertTimestampToDate"
|
@input="convertTimestampToDate"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="请输入时间戳"
|
:placeholder="t('timestamp.placeholderTs')"
|
||||||
class="input-field"
|
class="input-field"
|
||||||
/>
|
/>
|
||||||
<span class="arrow">→</span>
|
<span class="arrow">→</span>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
readonly
|
readonly
|
||||||
class="input-field readonly"
|
class="input-field readonly"
|
||||||
/>
|
/>
|
||||||
<button @click="copyToClipboard(dateStringOutput)" class="copy-btn" title="复制">
|
<button @click="copyToClipboard(dateStringOutput)" class="copy-btn" :title="t('common.copy')">
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,13 +86,13 @@
|
|||||||
|
|
||||||
<!-- 当前时间戳显示与控制 -->
|
<!-- 当前时间戳显示与控制 -->
|
||||||
<div class="current-timestamp-row">
|
<div class="current-timestamp-row">
|
||||||
<div class="conversion-label">当前时间戳:</div>
|
<div class="conversion-label">{{ t('timestamp.currentTs') }}</div>
|
||||||
<div class="current-timestamp-controls">
|
<div class="current-timestamp-controls">
|
||||||
<span class="current-timestamp-value">{{ currentTimestampDisplay }}</span>
|
<span class="current-timestamp-value">{{ currentTimestampDisplay }}</span>
|
||||||
<button @click="togglePause" class="control-btn-icon" :title="isPaused ? '继续' : '暂停'">
|
<button @click="togglePause" class="control-btn-icon" :title="isPaused ? t('timestamp.resume') : t('timestamp.pause')">
|
||||||
<i :class="isPaused ? 'fas fa-play' : 'fas fa-pause'"></i>
|
<i :class="isPaused ? 'fas fa-play' : 'fas fa-pause'"></i>
|
||||||
</button>
|
</button>
|
||||||
<button @click="resetData" class="control-btn-icon" title="重置数据">
|
<button @click="resetData" class="control-btn-icon" :title="t('timestamp.resetData')">
|
||||||
<i class="fas fa-rotate-right"></i>
|
<i class="fas fa-rotate-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<i v-else class="fas fa-circle-check"></i>
|
<i v-else class="fas fa-circle-check"></i>
|
||||||
<span>{{ toastMessage }}</span>
|
<span>{{ toastMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="closeToast" class="toast-close-btn" title="关闭">
|
<button @click="closeToast" class="toast-close-btn" :title="t('common.close')">
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,56 +123,33 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import DateTimePicker from '@/components/DateTimePicker.vue'
|
import DateTimePicker from '@/components/DateTimePicker.vue'
|
||||||
|
|
||||||
|
const { t, tm } = useI18n()
|
||||||
|
|
||||||
// 精度选项
|
// 精度选项
|
||||||
const precisionOptions = [
|
const precisionOptions = computed(() => [
|
||||||
{ value: 'seconds', label: '秒' },
|
{ value: 'seconds', label: t('timestamp.seconds') },
|
||||||
{ value: 'milliseconds', label: '毫秒' },
|
{ value: 'milliseconds', label: t('timestamp.milliseconds') },
|
||||||
{ value: 'nanoseconds', label: '纳秒' }
|
{ value: 'nanoseconds', label: t('timestamp.nanoseconds') },
|
||||||
|
])
|
||||||
|
|
||||||
|
const TIMEZONE_VALUES = [
|
||||||
|
'UTC-12:00', 'UTC-11:00', 'UTC-10:00', 'UTC-09:30', 'UTC-09:00', 'UTC-08:00', 'UTC-07:00', 'UTC-06:00',
|
||||||
|
'UTC-05:00', 'UTC-04:00', 'UTC-03:30', 'UTC-03:00', 'UTC-02:00', 'UTC-01:00', 'UTC+00:00', 'UTC+01:00',
|
||||||
|
'UTC+02:00', 'UTC+03:00', 'UTC+03:30', 'UTC+04:00', 'UTC+04:30', 'UTC+05:00', 'UTC+05:30', 'UTC+05:45',
|
||||||
|
'UTC+06:00', 'UTC+06:30', 'UTC+07:00', 'UTC+08:00', 'UTC+08:45', 'UTC+09:00', 'UTC+09:30', 'UTC+10:00',
|
||||||
|
'UTC+10:30', 'UTC+11:00', 'UTC+12:00', 'UTC+12:45', 'UTC+13:00', 'UTC+14:00',
|
||||||
]
|
]
|
||||||
|
|
||||||
// 所有时区选项
|
const timezoneOptions = computed(() => {
|
||||||
const timezoneOptions = [
|
const tzNames = tm('timestampTz') || {}
|
||||||
{ value: 'UTC-12:00', label: 'UTC-12:00 | 贝克岛' },
|
return TIMEZONE_VALUES.map(value => ({
|
||||||
{ value: 'UTC-11:00', label: 'UTC-11:00 | 萨摩亚' },
|
value,
|
||||||
{ value: 'UTC-10:00', label: 'UTC-10:00 | 夏威夷' },
|
label: `${value} | ${tzNames[value] || value}`,
|
||||||
{ value: 'UTC-09:30', label: 'UTC-09:30 | 马克萨斯群岛' },
|
}))
|
||||||
{ value: 'UTC-09:00', label: 'UTC-09:00 | 阿拉斯加' },
|
})
|
||||||
{ value: 'UTC-08:00', label: 'UTC-08:00 | 洛杉矶' },
|
|
||||||
{ value: 'UTC-07:00', label: 'UTC-07:00 | 丹佛' },
|
|
||||||
{ value: 'UTC-06:00', label: 'UTC-06:00 | 芝加哥' },
|
|
||||||
{ value: 'UTC-05:00', label: 'UTC-05:00 | 纽约' },
|
|
||||||
{ value: 'UTC-04:00', label: 'UTC-04:00 | 加拉加斯' },
|
|
||||||
{ value: 'UTC-03:30', label: 'UTC-03:30 | 纽芬兰' },
|
|
||||||
{ value: 'UTC-03:00', label: 'UTC-03:00 | 布宜诺斯艾利斯' },
|
|
||||||
{ value: 'UTC-02:00', label: 'UTC-02:00 | 大西洋中部' },
|
|
||||||
{ value: 'UTC-01:00', label: 'UTC-01:00 | 亚速尔群岛' },
|
|
||||||
{ value: 'UTC+00:00', label: 'UTC+00:00 | 伦敦' },
|
|
||||||
{ value: 'UTC+01:00', label: 'UTC+01:00 | 巴黎' },
|
|
||||||
{ value: 'UTC+02:00', label: 'UTC+02:00 | 开罗' },
|
|
||||||
{ value: 'UTC+03:00', label: 'UTC+03:00 | 莫斯科' },
|
|
||||||
{ value: 'UTC+03:30', label: 'UTC+03:30 | 德黑兰' },
|
|
||||||
{ value: 'UTC+04:00', label: 'UTC+04:00 | 迪拜' },
|
|
||||||
{ value: 'UTC+04:30', label: 'UTC+04:30 | 喀布尔' },
|
|
||||||
{ value: 'UTC+05:00', label: 'UTC+05:00 | 伊斯兰堡' },
|
|
||||||
{ value: 'UTC+05:30', label: 'UTC+05:30 | 新德里' },
|
|
||||||
{ value: 'UTC+05:45', label: 'UTC+05:45 | 加德满都' },
|
|
||||||
{ value: 'UTC+06:00', label: 'UTC+06:00 | 达卡' },
|
|
||||||
{ value: 'UTC+06:30', label: 'UTC+06:30 | 仰光' },
|
|
||||||
{ value: 'UTC+07:00', label: 'UTC+07:00 | 曼谷' },
|
|
||||||
{ value: 'UTC+08:00', label: 'UTC+08:00 | 北京' },
|
|
||||||
{ value: 'UTC+08:45', label: 'UTC+08:45 | 尤克拉' },
|
|
||||||
{ value: 'UTC+09:00', label: 'UTC+09:00 | 东京' },
|
|
||||||
{ value: 'UTC+09:30', label: 'UTC+09:30 | 阿德莱德' },
|
|
||||||
{ value: 'UTC+10:00', label: 'UTC+10:00 | 悉尼' },
|
|
||||||
{ value: 'UTC+10:30', label: 'UTC+10:30 | 豪勋爵岛' },
|
|
||||||
{ value: 'UTC+11:00', label: 'UTC+11:00 | 新喀里多尼亚' },
|
|
||||||
{ value: 'UTC+12:00', label: 'UTC+12:00 | 奥克兰' },
|
|
||||||
{ value: 'UTC+12:45', label: 'UTC+12:45 | 查塔姆群岛' },
|
|
||||||
{ value: 'UTC+13:00', label: 'UTC+13:00 | 萨摩亚' },
|
|
||||||
{ value: 'UTC+14:00', label: 'UTC+14:00 | 基里巴斯' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 当前时间相关
|
// 当前时间相关
|
||||||
const currentTime = ref(new Date())
|
const currentTime = ref(new Date())
|
||||||
@@ -182,8 +159,8 @@ const isPaused = ref(false)
|
|||||||
// 时区相关
|
// 时区相关
|
||||||
const timezone = ref('UTC+08:00')
|
const timezone = ref('UTC+08:00')
|
||||||
const timezoneLabel = computed(() => {
|
const timezoneLabel = computed(() => {
|
||||||
const tz = timezoneOptions.find(opt => opt.value === timezone.value)
|
const tz = timezoneOptions.value.find(opt => opt.value === timezone.value)
|
||||||
return tz ? tz.label.split('|')[1].trim() : '北京'
|
return tz ? tz.label.split('|')[1].trim() : ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 时间戳转时间
|
// 时间戳转时间
|
||||||
@@ -234,13 +211,9 @@ const currentTimestampDisplay = computed(() => {
|
|||||||
|
|
||||||
// 获取日期输入框的placeholder
|
// 获取日期输入框的placeholder
|
||||||
const getDatePlaceholder = () => {
|
const getDatePlaceholder = () => {
|
||||||
if (timestampType.value === 'seconds') {
|
if (timestampType.value === 'seconds') return t('timestamp.datePlaceholderSeconds')
|
||||||
return '格式:yyyy-MM-dd HH:mm:ss'
|
if (timestampType.value === 'milliseconds') return t('timestamp.datePlaceholderMs')
|
||||||
} else if (timestampType.value === 'milliseconds') {
|
return t('timestamp.datePlaceholderNs')
|
||||||
return '格式:yyyy-MM-dd HH:mm:ss.SSS'
|
|
||||||
} else {
|
|
||||||
return '格式:yyyy-MM-dd HH:mm:ss.SSSSSSSSS'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新时间
|
// 更新时间
|
||||||
@@ -265,7 +238,7 @@ const resetData = () => {
|
|||||||
dateStringInput.value = ''
|
dateStringInput.value = ''
|
||||||
timestampOutput.value = ''
|
timestampOutput.value = ''
|
||||||
currentTime.value = new Date()
|
currentTime.value = new Date()
|
||||||
showToast('数据已重置', 'success')
|
showToast(t('timestamp.dataReset'), 'success')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间戳转时间字符串
|
// 时间戳转时间字符串
|
||||||
@@ -296,7 +269,7 @@ const convertTimestampToDate = () => {
|
|||||||
nanoseconds = Number(timestampNs % BigInt(1000000))
|
nanoseconds = Number(timestampNs % BigInt(1000000))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dateStringOutput.value = ''
|
dateStringOutput.value = ''
|
||||||
showToast('请输入有效的纳秒级时间戳', 'error')
|
showToast(t('timestamp.invalidNs'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -304,7 +277,7 @@ const convertTimestampToDate = () => {
|
|||||||
|
|
||||||
if (isNaN(timestamp)) {
|
if (isNaN(timestamp)) {
|
||||||
dateStringOutput.value = ''
|
dateStringOutput.value = ''
|
||||||
showToast('请输入有效的数字', 'error')
|
showToast(t('timestamp.invalidNumber'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +298,7 @@ const convertTimestampToDate = () => {
|
|||||||
|
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
dateStringOutput.value = ''
|
dateStringOutput.value = ''
|
||||||
showToast('无效的时间戳', 'error')
|
showToast(t('timestamp.invalidTs'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +321,7 @@ const convertTimestampToDate = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dateStringOutput.value = ''
|
dateStringOutput.value = ''
|
||||||
showToast('转换失败:' + error.message, 'error')
|
showToast(t('timestamp.convertFailed') + error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +366,7 @@ const convertDateToTimestamp = () => {
|
|||||||
|
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
timestampOutput.value = ''
|
timestampOutput.value = ''
|
||||||
showToast('无效的时间格式', 'error')
|
showToast(t('timestamp.invalidDateFormat'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +381,7 @@ const convertDateToTimestamp = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
timestampOutput.value = ''
|
timestampOutput.value = ''
|
||||||
showToast('转换失败:' + error.message, 'error')
|
showToast(t('timestamp.convertFailed') + error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,13 +394,13 @@ const handleDateTimeConfirm = (value) => {
|
|||||||
// 复制到剪贴板
|
// 复制到剪贴板
|
||||||
const copyToClipboard = async (text) => {
|
const copyToClipboard = async (text) => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
showToast('没有可复制的内容', 'error')
|
showToast(t('timestamp.noContentToCopy'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
showToast('已复制到剪贴板', 'success')
|
showToast(t('common.copied'), 'success')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 降级方案
|
// 降级方案
|
||||||
const textArea = document.createElement('textarea')
|
const textArea = document.createElement('textarea')
|
||||||
@@ -438,9 +411,9 @@ const copyToClipboard = async (text) => {
|
|||||||
textArea.select()
|
textArea.select()
|
||||||
try {
|
try {
|
||||||
document.execCommand('copy')
|
document.execCommand('copy')
|
||||||
showToast('已复制到剪贴板', 'success')
|
showToast(t('common.copied'), 'success')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('复制失败', 'error')
|
showToast(t('timestamp.copyFailed'), 'error')
|
||||||
}
|
}
|
||||||
document.body.removeChild(textArea)
|
document.body.removeChild(textArea)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<i v-else class="fas fa-circle-check"></i>
|
<i v-else class="fas fa-circle-check"></i>
|
||||||
<span>{{ toastMessage }}</span>
|
<span>{{ toastMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button @click="closeToast" class="toast-close-btn" title="关闭">
|
<button @click="closeToast" class="toast-close-btn" :title="t('common.close')">
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
@input="convertVariableName"
|
@input="convertVariableName"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="请输入变量名(支持任意格式)"
|
:placeholder="t('variable.placeholder')"
|
||||||
class="input-field"
|
class="input-field"
|
||||||
/>
|
/>
|
||||||
<button @click="clearInput" class="clear-btn" title="清空">
|
<button @click="clearInput" class="clear-btn" :title="t('common.clear')">
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<div class="output-section">
|
<div class="output-section">
|
||||||
<div class="output-row">
|
<div class="output-row">
|
||||||
<div
|
<div
|
||||||
v-for="format in formats.slice(0, 3)"
|
v-for="format in formatList.slice(0, 3)"
|
||||||
:key="format.key"
|
:key="format.key"
|
||||||
class="output-item"
|
class="output-item"
|
||||||
>
|
>
|
||||||
@@ -45,19 +45,19 @@
|
|||||||
<button
|
<button
|
||||||
@click="copyToClipboard(format.value, format.label)"
|
@click="copyToClipboard(format.value, format.label)"
|
||||||
class="copy-btn"
|
class="copy-btn"
|
||||||
:title="`复制${format.label}`"
|
:title="t('variable.copyLabel', { label: format.label })"
|
||||||
>
|
>
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="output-value" :class="{ empty: !format.value }">
|
<div class="output-value" :class="{ empty: !format.value }">
|
||||||
{{ format.value || '—' }}
|
{{ format.value || t('variable.empty') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="output-row">
|
<div class="output-row">
|
||||||
<div
|
<div
|
||||||
v-for="format in formats.slice(3)"
|
v-for="format in formatList.slice(3)"
|
||||||
:key="format.key"
|
:key="format.key"
|
||||||
class="output-item"
|
class="output-item"
|
||||||
>
|
>
|
||||||
@@ -66,13 +66,13 @@
|
|||||||
<button
|
<button
|
||||||
@click="copyToClipboard(format.value, format.label)"
|
@click="copyToClipboard(format.value, format.label)"
|
||||||
class="copy-btn"
|
class="copy-btn"
|
||||||
:title="`复制${format.label}`"
|
:title="t('variable.copyLabel', { label: format.label })"
|
||||||
>
|
>
|
||||||
<i class="far fa-copy"></i>
|
<i class="far fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="output-value" :class="{ empty: !format.value }">
|
<div class="output-value" :class="{ empty: !format.value }">
|
||||||
{{ format.value || '—' }}
|
{{ format.value || t('variable.empty') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,20 +84,38 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
const inputText = ref('')
|
const inputText = ref('')
|
||||||
const toastMessage = ref('')
|
const toastMessage = ref('')
|
||||||
const toastType = ref('success')
|
const toastType = ref('success')
|
||||||
let toastTimer = null
|
let toastTimer = null
|
||||||
|
|
||||||
// 变量名格式定义
|
const formatKeys = [
|
||||||
const formats = ref([
|
{ key: 'camelCase', labelKey: 'variable.camelCase' },
|
||||||
{ key: 'camelCase', label: '小驼峰 (camelCase)', value: '' },
|
{ key: 'PascalCase', labelKey: 'variable.pascalCase' },
|
||||||
{ key: 'PascalCase', label: '大驼峰 (PascalCase)', value: '' },
|
{ key: 'snake_case', labelKey: 'variable.snakeCase' },
|
||||||
{ key: 'snake_case', label: '下划线 (snake_case)', value: '' },
|
{ key: 'kebab-case', labelKey: 'variable.kebabCase' },
|
||||||
{ key: 'kebab-case', label: '横线 (kebab-case)', value: '' },
|
{ key: 'CONSTANT_CASE', labelKey: 'variable.constantCase' },
|
||||||
{ key: 'CONSTANT_CASE', label: '常量 (CONSTANT_CASE)', value: '' }
|
]
|
||||||
])
|
|
||||||
|
const formatValues = ref({
|
||||||
|
camelCase: '',
|
||||||
|
PascalCase: '',
|
||||||
|
snake_case: '',
|
||||||
|
'kebab-case': '',
|
||||||
|
CONSTANT_CASE: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatList = computed(() =>
|
||||||
|
formatKeys.map(({ key, labelKey }) => ({
|
||||||
|
key,
|
||||||
|
label: t(labelKey),
|
||||||
|
value: formatValues.value[key],
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
// 显示提示
|
// 显示提示
|
||||||
const showToast = (message, type = 'success', duration = 3000) => {
|
const showToast = (message, type = 'success', duration = 3000) => {
|
||||||
@@ -214,31 +232,23 @@ const convertVariableName = () => {
|
|||||||
const words = parseToWords(inputText.value)
|
const words = parseToWords(inputText.value)
|
||||||
|
|
||||||
if (words.length === 0) {
|
if (words.length === 0) {
|
||||||
formats.value.forEach(format => {
|
formatValues.value = {
|
||||||
format.value = ''
|
camelCase: '',
|
||||||
})
|
PascalCase: '',
|
||||||
|
snake_case: '',
|
||||||
|
'kebab-case': '',
|
||||||
|
CONSTANT_CASE: '',
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
formats.value.forEach(format => {
|
formatValues.value = {
|
||||||
switch (format.key) {
|
camelCase: toCamelCase(words),
|
||||||
case 'camelCase':
|
PascalCase: toPascalCase(words),
|
||||||
format.value = toCamelCase(words)
|
snake_case: toSnakeCase(words),
|
||||||
break
|
'kebab-case': toKebabCase(words),
|
||||||
case 'PascalCase':
|
CONSTANT_CASE: toConstantCase(words),
|
||||||
format.value = toPascalCase(words)
|
}
|
||||||
break
|
|
||||||
case 'snake_case':
|
|
||||||
format.value = toSnakeCase(words)
|
|
||||||
break
|
|
||||||
case 'kebab-case':
|
|
||||||
format.value = toKebabCase(words)
|
|
||||||
break
|
|
||||||
case 'CONSTANT_CASE':
|
|
||||||
format.value = toConstantCase(words)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空输入
|
// 清空输入
|
||||||
@@ -249,16 +259,16 @@ const clearInput = () => {
|
|||||||
|
|
||||||
// 复制到剪贴板
|
// 复制到剪贴板
|
||||||
const copyToClipboard = async (text, label) => {
|
const copyToClipboard = async (text, label) => {
|
||||||
if (!text || text === '—') {
|
if (!text || text === t('variable.empty')) {
|
||||||
showToast('没有可复制的内容', 'error')
|
showToast(t('variable.noContentToCopy'), 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
showToast(`${label}已复制到剪贴板`, 'success', 2000)
|
showToast(t('variable.copiedLabel', { label }), 'success', 2000)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('复制失败:' + error.message, 'error')
|
showToast(t('common.copyFailed') + error.message, 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user