forked from zguiy/utils
工具完成
This commit is contained in:
847
src/components/tools/JsonFormatter.vue
Normal file
847
src/components/tools/JsonFormatter.vue
Normal file
@ -0,0 +1,847 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 验证状态显示 -->
|
||||
<div v-if="validationResult.message || isLoading" class="text-center">
|
||||
<div v-if="isLoading" class="flex items-center justify-center text-tertiary">
|
||||
<FontAwesomeIcon :icon="['fas', 'spinner']" class="animate-spin mr-2" />
|
||||
<span>{{ isLargeJson ? t('tools.json_formatter.processing_large_json') : t('tools.json_formatter.parsing_json') }}</span>
|
||||
</div>
|
||||
<div v-else-if="validationResult.message" :class="[
|
||||
'flex items-center justify-center space-x-2',
|
||||
validationResult.isValid ? 'text-success' : 'text-error'
|
||||
]">
|
||||
<FontAwesomeIcon
|
||||
:icon="validationResult.isValid ? ['fas', 'check'] : ['fas', 'times']"
|
||||
/>
|
||||
<span>{{ validationResult.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="card p-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@click="toggleCompression"
|
||||
:disabled="isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="isCompressed ? ['fas', 'expand'] : ['fas', 'compress']" />
|
||||
<span>{{ isCompressed ? t('tools.json_formatter.beautify') : t('tools.json_formatter.compress') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="toggleFoldable"
|
||||
:disabled="isLoading || !jsonOutput"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="isFoldable ? ['fas', 'folder'] : ['fas', 'folder-open']" />
|
||||
<span>{{ isFoldable ? t('tools.json_formatter.normal_mode') : t('tools.json_formatter.fold_mode') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="copyToClipboard"
|
||||
:disabled="!jsonOutput || isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="copied ? ['fas', 'check'] : ['fas', 'copy']" />
|
||||
<span>{{ copied ? t('common.copySuccess') : t('tools.json_formatter.copy') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="clearInput"
|
||||
:disabled="isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'trash']" />
|
||||
<span>{{ t('tools.json_formatter.clear') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="loadExample"
|
||||
:disabled="isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'code']" />
|
||||
<span>{{ t('tools.json_formatter.load_example') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="reformat"
|
||||
:disabled="!jsonInput || isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="isLoading ? ['fas', 'spinner'] : ['fas', 'sync']" :class="isLoading && 'animate-spin'" />
|
||||
<span>{{ isLoading ? t('tools.json_formatter.processing') : t('tools.json_formatter.reformat') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openSaveModal"
|
||||
:disabled="!jsonOutput || isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'save']" />
|
||||
<span>{{ t('tools.json_formatter.save') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="openHistory"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'history']" />
|
||||
<span>{{ t('tools.json_formatter.history') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="removeSlashes"
|
||||
:disabled="!jsonInput || isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'eraser']" />
|
||||
<span>{{ t('tools.json_formatter.remove_slash') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="escapeString"
|
||||
:disabled="!jsonInput || isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'code']" />
|
||||
<span>{{ t('tools.json_formatter.escape_string') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="unescapeString"
|
||||
:disabled="!jsonInput || isLoading"
|
||||
class="btn-secondary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'undo']" />
|
||||
<span>{{ t('tools.json_formatter.unescape_string') }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="isLoading"
|
||||
@click="cancelFormatting"
|
||||
class="btn-primary flex items-center space-x-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'times']" />
|
||||
<span>{{ t('tools.json_formatter.cancel') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- 输入区域 -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-sm font-medium text-secondary">{{ t('tools.json_formatter.input_json') }}</label>
|
||||
<div class="text-xs text-tertiary">{{ t('tools.json_formatter.paste_json_here') }}</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-model="jsonInput"
|
||||
ref="jsonInputRef"
|
||||
:placeholder="t('tools.json_formatter.paste_json_placeholder')"
|
||||
class="textarea-field h-96 font-mono text-sm"
|
||||
:disabled="isLoading"
|
||||
@input="handleInputChange"
|
||||
@blur="handleBlur"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
|
||||
<div v-if="errorMessage" class="mt-2 text-sm text-error">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出区域 -->
|
||||
<div class="card p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-sm font-medium text-secondary">{{ t('tools.json_formatter.output') }}</label>
|
||||
<div class="text-xs text-tertiary">
|
||||
<span v-if="jsonOutput && !isLoading">{{ jsonOutput.length.toLocaleString() }} {{ t('tools.json_formatter.characters') }}</span>
|
||||
<span v-if="isLoading" class="flex items-center">
|
||||
<FontAwesomeIcon :icon="['fas', 'spinner']" class="animate-spin mr-1" />
|
||||
{{ isLargeJson ? t('tools.json_formatter.processing_large_json') : t('tools.json_formatter.processing') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative h-96 border border-primary/20 rounded-lg overflow-hidden bg-secondary/5">
|
||||
<div v-if="isLoading" class="absolute inset-0 flex items-center justify-center bg-secondary/10 backdrop-blur-sm z-10">
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<FontAwesomeIcon :icon="['fas', 'spinner']" class="animate-spin text-2xl text-primary" />
|
||||
<span class="text-secondary text-center">
|
||||
{{ isLargeJson ? t('tools.json_formatter.processing_large_json_message') : t('tools.json_formatter.parsing_json') }}
|
||||
</span>
|
||||
<button @click="cancelFormatting" class="mt-3 px-3 py-1.5 text-xs rounded btn-secondary">
|
||||
{{ t('tools.json_formatter.cancel_processing') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="jsonOutput" class="h-full overflow-auto p-4">
|
||||
<div v-if="isFoldable && parsedJson" class="json-viewer">
|
||||
<!-- 可折叠的JSON视图 - 简化版本 -->
|
||||
<JsonTreeView :data="parsedJson" />
|
||||
</div>
|
||||
<pre v-else class="whitespace-pre-wrap text-sm font-mono text-primary leading-relaxed">{{ jsonOutput }}</pre>
|
||||
</div>
|
||||
|
||||
<div v-else class="h-full flex items-center justify-center text-tertiary">
|
||||
<span>{{ t('tools.json_formatter.output_placeholder') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JSONPath查询 -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.json_formatter.jsonpath_query') }}</h3>
|
||||
<div class="text-xs text-tertiary mb-3">{{ t('tools.json_formatter.enter_jsonpath') }}</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="relative">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'search']"
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-tertiary"
|
||||
/>
|
||||
<input
|
||||
v-model="jsonPath"
|
||||
ref="jsonPathInputRef"
|
||||
type="text"
|
||||
:placeholder="t('tools.json_formatter.jsonpath_placeholder')"
|
||||
class="input-field pl-10"
|
||||
:disabled="isLoading || !jsonOutput"
|
||||
@keyup.enter="queryJsonPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="p-3 rounded-lg min-h-[40px] text-sm bg-secondary/10 border border-primary/10">
|
||||
<pre v-if="pathResult" class="whitespace-pre-wrap text-secondary">{{ pathResult }}</pre>
|
||||
<span v-else class="text-tertiary">{{ t('tools.json_formatter.query_result_placeholder') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录侧边栏 -->
|
||||
<div v-if="isHistoryOpen" class="fixed inset-0 z-50">
|
||||
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" @click="closeHistory"></div>
|
||||
<div class="absolute top-0 right-0 h-full w-96 bg-card border-l border-primary/20 shadow-xl overflow-y-auto">
|
||||
<div class="p-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-primary">{{ t('tools.json_formatter.history') }}</h3>
|
||||
<button @click="closeHistory" class="p-2 rounded text-secondary hover:text-primary">
|
||||
<FontAwesomeIcon :icon="['fas', 'times']" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="historyItems.length === 0" class="text-center text-tertiary py-8">
|
||||
<FontAwesomeIcon :icon="['fas', 'history']" class="text-4xl mb-2" />
|
||||
<p>{{ t('tools.json_formatter.no_history') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- 收藏的项目 -->
|
||||
<div v-if="favoriteItems.length > 0" class="mb-6">
|
||||
<h4 class="text-sm font-medium text-secondary mb-2">{{ t('tools.json_formatter.favorites') }}</h4>
|
||||
<div v-for="item in favoriteItems" :key="item.id" class="history-item" @click="loadFromHistory(item)">
|
||||
<div class="flex-1 truncate">
|
||||
<div class="font-medium truncate text-primary">{{ item.title }}</div>
|
||||
<div class="text-xs text-tertiary">{{ formatTimestamp(item.timestamp) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button @click.stop="toggleFavorite(item.id)" class="text-warning" :title="t('tools.json_formatter.remove_favorite')">
|
||||
<FontAwesomeIcon :icon="['fas', 'star']" />
|
||||
</button>
|
||||
<button @click.stop="startEditingTitle(item)" class="text-tertiary hover:text-primary" :title="t('tools.json_formatter.edit_title')">
|
||||
<FontAwesomeIcon :icon="['fas', 'edit']" />
|
||||
</button>
|
||||
<button @click.stop="deleteHistoryItem(item.id)" class="text-tertiary hover:text-error" :title="t('tools.json_formatter.delete')">
|
||||
<FontAwesomeIcon :icon="['fas', 'trash']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 所有历史记录 -->
|
||||
<h4 class="text-sm font-medium text-secondary mb-2">{{ t('tools.json_formatter.all_history') }}</h4>
|
||||
<div v-for="item in historyItems" :key="item.id" class="history-item" @click="loadFromHistory(item)">
|
||||
<div class="flex-1 truncate">
|
||||
<div class="font-medium truncate text-primary">{{ item.title }}</div>
|
||||
<div class="text-xs text-tertiary">{{ formatTimestamp(item.timestamp) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click.stop="toggleFavorite(item.id)"
|
||||
:class="item.isFavorite ? 'text-warning' : 'text-tertiary hover:text-warning'"
|
||||
:title="item.isFavorite ? t('tools.json_formatter.remove_favorite') : t('tools.json_formatter.add_favorite')"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'star']" />
|
||||
</button>
|
||||
<button @click.stop="startEditingTitle(item)" class="text-tertiary hover:text-primary" :title="t('tools.json_formatter.edit_title')">
|
||||
<FontAwesomeIcon :icon="['fas', 'edit']" />
|
||||
</button>
|
||||
<button @click.stop="deleteHistoryItem(item.id)" class="text-tertiary hover:text-error" :title="t('tools.json_formatter.delete')">
|
||||
<FontAwesomeIcon :icon="['fas', 'trash']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保存模态框 -->
|
||||
<div v-if="isSaveModalOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" @click="closeSaveModal"></div>
|
||||
<div class="relative bg-card rounded-lg p-6 w-full max-w-md mx-4 border border-primary/20 shadow-xl">
|
||||
<h3 class="text-lg font-medium text-primary mb-4">
|
||||
{{ editingItem ? t('tools.json_formatter.edit_saved_json') : t('tools.json_formatter.save_to_history') }}
|
||||
</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-secondary mb-1">{{ t('tools.json_formatter.modal_title') }}</label>
|
||||
<input
|
||||
v-model="savingTitle"
|
||||
type="text"
|
||||
:placeholder="t('tools.json_formatter.enter_title')"
|
||||
class="input-field w-full"
|
||||
@keyup.enter="saveToHistory"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button @click="closeSaveModal" class="btn-secondary">
|
||||
{{ t('tools.json_formatter.cancel') }}
|
||||
</button>
|
||||
<button @click="saveToHistory" class="btn-primary" :disabled="!savingTitle.trim()">
|
||||
{{ editingItem ? t('tools.json_formatter.update') : t('tools.json_formatter.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用指南 -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-lg font-semibold text-primary mb-3">{{ t('tools.json_formatter.usage_guide') }}</h3>
|
||||
<ul class="text-sm text-tertiary space-y-1 list-disc pl-5">
|
||||
<li>{{ t('tools.json_formatter.guide_1') }}</li>
|
||||
<li>{{ t('tools.json_formatter.guide_2') }}</li>
|
||||
<li>{{ t('tools.json_formatter.guide_3') }}</li>
|
||||
<li>{{ t('tools.json_formatter.guide_4') }}</li>
|
||||
<li>{{ t('tools.json_formatter.guide_5') }}</li>
|
||||
<li>{{ t('tools.json_formatter.guide_6') }}</li>
|
||||
<li>{{ t('tools.json_formatter.guide_7') }}</li>
|
||||
<li>{{ t('tools.json_formatter.guide_8') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { useLanguage } from '@/composables/useLanguage'
|
||||
|
||||
const { t } = useLanguage()
|
||||
|
||||
// 历史记录项目类型
|
||||
interface JsonHistoryItem {
|
||||
id: string
|
||||
title: string
|
||||
json: string
|
||||
timestamp: number
|
||||
isFavorite?: boolean
|
||||
}
|
||||
|
||||
// 响应式状态
|
||||
const jsonInput = ref('')
|
||||
const jsonOutput = ref('')
|
||||
const jsonPath = ref('')
|
||||
const pathResult = ref('')
|
||||
const errorMessage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const isCompressed = ref(false)
|
||||
const isFoldable = ref(true)
|
||||
const copied = ref(false)
|
||||
const isLargeJson = ref(false)
|
||||
const validationResult = ref({
|
||||
isValid: false,
|
||||
message: ''
|
||||
})
|
||||
|
||||
// 历史记录相关状态
|
||||
const historyItems = ref<JsonHistoryItem[]>([])
|
||||
const isHistoryOpen = ref(false)
|
||||
const isSaveModalOpen = ref(false)
|
||||
const savingTitle = ref('')
|
||||
const editingItem = ref<JsonHistoryItem | null>(null)
|
||||
|
||||
// refs
|
||||
const jsonInputRef = ref<HTMLTextAreaElement>()
|
||||
const jsonPathInputRef = ref<HTMLInputElement>()
|
||||
|
||||
// 处理参考,用于取消操作
|
||||
const processingRef = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const parsedJson = computed(() => {
|
||||
if (!jsonOutput.value) return null
|
||||
try {
|
||||
return JSON.parse(jsonOutput.value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const favoriteItems = computed(() => {
|
||||
return historyItems.value.filter(item => item.isFavorite)
|
||||
})
|
||||
|
||||
// 从本地存储加载历史记录
|
||||
onMounted(() => {
|
||||
const savedHistory = localStorage.getItem('json_formatter_history')
|
||||
if (savedHistory) {
|
||||
try {
|
||||
historyItems.value = JSON.parse(savedHistory)
|
||||
} catch (e) {
|
||||
console.error('加载历史记录失败:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 保存历史记录到本地存储
|
||||
const saveHistoryToLocalStorage = () => {
|
||||
localStorage.setItem('json_formatter_history', JSON.stringify(historyItems.value))
|
||||
}
|
||||
|
||||
// 格式化JSON
|
||||
const formatJson = (json: string, compress = false) => {
|
||||
if (!json.trim()) {
|
||||
jsonOutput.value = ''
|
||||
validationResult.value = { isValid: false, message: '' }
|
||||
return
|
||||
}
|
||||
|
||||
// 检查JSON大小
|
||||
const isLarge = json.length > 100000
|
||||
isLargeJson.value = isLarge
|
||||
|
||||
// 设置加载状态
|
||||
isLoading.value = true
|
||||
processingRef.value = true
|
||||
|
||||
// 使用setTimeout确保UI更新
|
||||
setTimeout(() => {
|
||||
if (!processingRef.value) return // 检查是否被取消
|
||||
|
||||
try {
|
||||
// 处理可能的JS对象文本
|
||||
const processedJson = json
|
||||
.replace(/(['"])?([a-zA-Z0-9_]+)(['"])?:/g, '"$2":')
|
||||
.replace(/'/g, '"')
|
||||
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(processedJson)
|
||||
} catch (e) {
|
||||
// 尝试使用Function构造器处理JS对象
|
||||
try {
|
||||
parsed = new Function('return ' + json)()
|
||||
} catch {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// 根据模式输出不同格式
|
||||
let formattedJson
|
||||
if (compress) {
|
||||
formattedJson = JSON.stringify(parsed)
|
||||
} else {
|
||||
formattedJson = JSON.stringify(parsed, null, 2)
|
||||
}
|
||||
|
||||
if (!processingRef.value) return // 再次检查是否被取消
|
||||
|
||||
// 设置输出
|
||||
jsonOutput.value = formattedJson
|
||||
|
||||
// 计算大小
|
||||
const sizeKB = (formattedJson.length / 1024).toFixed(1)
|
||||
const largeJsonMessage = t('tools.json_formatter.large_json_processed').replace('{size}', sizeKB)
|
||||
|
||||
validationResult.value = {
|
||||
isValid: true,
|
||||
message: isLarge ? largeJsonMessage : t('tools.json_formatter.json_valid')
|
||||
}
|
||||
errorMessage.value = ''
|
||||
|
||||
// 如果有JSONPath查询,执行查询
|
||||
if (jsonPath.value) {
|
||||
queryJsonPath()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (!processingRef.value) return
|
||||
|
||||
if (error instanceof Error) {
|
||||
errorMessage.value = error.message
|
||||
validationResult.value = { isValid: false, message: t('tools.json_formatter.json_invalid') }
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
processingRef.value = false
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// 切换压缩/美化
|
||||
const toggleCompression = () => {
|
||||
isCompressed.value = !isCompressed.value
|
||||
formatJson(jsonInput.value, isCompressed.value)
|
||||
}
|
||||
|
||||
// 切换折叠功能
|
||||
const toggleFoldable = () => {
|
||||
isFoldable.value = !isFoldable.value
|
||||
}
|
||||
|
||||
// 取消格式化操作
|
||||
const cancelFormatting = () => {
|
||||
processingRef.value = false
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// 移除JSON中的转义斜杠
|
||||
const removeSlashes = () => {
|
||||
if (!jsonInput.value) return
|
||||
|
||||
try {
|
||||
const processed = jsonInput.value.replace(/\\\//g, '/')
|
||||
if (processed === jsonInput.value) {
|
||||
console.log('没有检测到需要替换的内容')
|
||||
return
|
||||
}
|
||||
jsonInput.value = processed
|
||||
setTimeout(() => formatJson(processed, isCompressed.value), 100)
|
||||
} catch (error) {
|
||||
console.error('移除斜杠处理失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串转义
|
||||
const escapeString = () => {
|
||||
if (!jsonInput.value) return
|
||||
|
||||
try {
|
||||
const processed = jsonInput.value
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\t/g, '\\t')
|
||||
.replace(/\f/g, '\\f')
|
||||
.replace(/\b/g, '\\b')
|
||||
|
||||
if (processed === jsonInput.value) {
|
||||
console.log('没有检测到需要转义的内容')
|
||||
return
|
||||
}
|
||||
|
||||
jsonInput.value = processed
|
||||
} catch (error) {
|
||||
console.error('字符串转义处理失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 字符串反转义
|
||||
const unescapeString = () => {
|
||||
if (!jsonInput.value) return
|
||||
|
||||
try {
|
||||
const processed = jsonInput.value
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\r/g, '\r')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\f/g, '\f')
|
||||
.replace(/\\b/g, '\b')
|
||||
.replace(/\\\\/g, '\\')
|
||||
|
||||
if (processed === jsonInput.value) {
|
||||
console.log('没有检测到需要反转义的内容')
|
||||
return
|
||||
}
|
||||
|
||||
jsonInput.value = processed
|
||||
} catch (error) {
|
||||
console.error('字符串反转义处理失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制结果到剪贴板
|
||||
const copyToClipboard = async () => {
|
||||
if (!jsonOutput.value) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(jsonOutput.value)
|
||||
copied.value = true
|
||||
setTimeout(() => {
|
||||
copied.value = false
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空输入
|
||||
const clearInput = () => {
|
||||
if (isLoading.value) {
|
||||
cancelFormatting()
|
||||
}
|
||||
|
||||
jsonInput.value = ''
|
||||
jsonOutput.value = ''
|
||||
errorMessage.value = ''
|
||||
validationResult.value = { isValid: false, message: '' }
|
||||
pathResult.value = ''
|
||||
|
||||
nextTick(() => {
|
||||
jsonInputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
// 处理输入变化
|
||||
const handleInputChange = () => {
|
||||
if (validationResult.value.message) {
|
||||
validationResult.value = { isValid: false, message: '' }
|
||||
errorMessage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 处理失焦事件
|
||||
const handleBlur = () => {
|
||||
if (jsonInput.value && !isLoading.value) {
|
||||
formatJson(jsonInput.value, isCompressed.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理粘贴事件
|
||||
const handlePaste = async (e: Event) => {
|
||||
const clipboardEvent = e as ClipboardEvent
|
||||
const pastedText = clipboardEvent.clipboardData?.getData('text')
|
||||
|
||||
if (pastedText && pastedText.trim().length > 0) {
|
||||
if (isLoading.value) {
|
||||
cancelFormatting()
|
||||
}
|
||||
|
||||
jsonInput.value = pastedText
|
||||
isLoading.value = true
|
||||
processingRef.value = true
|
||||
setTimeout(() => formatJson(pastedText, isCompressed.value), 100)
|
||||
}
|
||||
}
|
||||
|
||||
// 路径查询变化
|
||||
const queryJsonPath = () => {
|
||||
if (!jsonPath.value || !jsonOutput.value) {
|
||||
pathResult.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonOutput.value)
|
||||
const result = getValueByPath(parsed, jsonPath.value)
|
||||
|
||||
if (typeof result === 'object' && result !== null) {
|
||||
pathResult.value = JSON.stringify(result, null, 2)
|
||||
} else {
|
||||
pathResult.value = String(result)
|
||||
}
|
||||
} catch (error) {
|
||||
pathResult.value = `查询错误: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
}
|
||||
}
|
||||
|
||||
// 通过路径获取值的辅助函数
|
||||
const getValueByPath = (obj: any, path: string): any => {
|
||||
const segments = path
|
||||
.replace(/\[(\w+)\]/g, '.$1')
|
||||
.replace(/^\./, '')
|
||||
.split('.')
|
||||
|
||||
let result = obj
|
||||
|
||||
for (const segment of segments) {
|
||||
if (typeof result === 'object' && result !== null && segment in result) {
|
||||
result = result[segment]
|
||||
} else {
|
||||
throw new Error(`路径 '${path}' 不存在`)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 加载示例JSON
|
||||
const loadExample = () => {
|
||||
if (isLoading.value) {
|
||||
cancelFormatting()
|
||||
}
|
||||
|
||||
const example = {
|
||||
name: "极速箱",
|
||||
version: "1.0.0",
|
||||
description: "高效开发工具集成平台",
|
||||
author: {
|
||||
name: "JiSuXiang开发团队",
|
||||
email: "support@jisuxiang.com"
|
||||
},
|
||||
features: [
|
||||
"JSON格式化与验证",
|
||||
"时间戳转换",
|
||||
"编码转换工具",
|
||||
"正则表达式测试"
|
||||
],
|
||||
statistics: {
|
||||
tools: 24,
|
||||
users: 100000,
|
||||
rating: 4.9
|
||||
},
|
||||
isOpenSource: true,
|
||||
lastUpdate: "2024-12-01T08:00:00Z"
|
||||
}
|
||||
|
||||
const exampleJson = JSON.stringify(example)
|
||||
jsonInput.value = exampleJson
|
||||
formatJson(exampleJson, isCompressed.value)
|
||||
}
|
||||
|
||||
// 重新格式化
|
||||
const reformat = () => {
|
||||
if (isLoading.value) {
|
||||
cancelFormatting()
|
||||
}
|
||||
formatJson(jsonInput.value, isCompressed.value)
|
||||
}
|
||||
|
||||
// 历史记录相关函数
|
||||
const openSaveModal = () => {
|
||||
savingTitle.value = `JSON ${new Date().toLocaleString()}`
|
||||
isSaveModalOpen.value = true
|
||||
}
|
||||
|
||||
const closeSaveModal = () => {
|
||||
isSaveModalOpen.value = false
|
||||
editingItem.value = null
|
||||
savingTitle.value = ''
|
||||
}
|
||||
|
||||
const openHistory = () => {
|
||||
isHistoryOpen.value = true
|
||||
}
|
||||
|
||||
const closeHistory = () => {
|
||||
isHistoryOpen.value = false
|
||||
}
|
||||
|
||||
const saveToHistory = () => {
|
||||
if (!jsonOutput.value || !savingTitle.value.trim()) return
|
||||
|
||||
if (editingItem.value) {
|
||||
// 更新现有项目
|
||||
const updatedItem = {
|
||||
...editingItem.value,
|
||||
title: savingTitle.value,
|
||||
json: jsonOutput.value,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
const index = historyItems.value.findIndex(item => item.id === editingItem.value!.id)
|
||||
if (index !== -1) {
|
||||
historyItems.value[index] = updatedItem
|
||||
}
|
||||
} else {
|
||||
// 创建新项目
|
||||
const newItem: JsonHistoryItem = {
|
||||
id: Date.now().toString(),
|
||||
title: savingTitle.value,
|
||||
json: jsonOutput.value,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
historyItems.value.unshift(newItem)
|
||||
}
|
||||
|
||||
saveHistoryToLocalStorage()
|
||||
closeSaveModal()
|
||||
}
|
||||
|
||||
const loadFromHistory = (item: JsonHistoryItem) => {
|
||||
jsonInput.value = item.json
|
||||
formatJson(item.json, isCompressed.value)
|
||||
closeHistory()
|
||||
}
|
||||
|
||||
const deleteHistoryItem = (id: string) => {
|
||||
historyItems.value = historyItems.value.filter(item => item.id !== id)
|
||||
saveHistoryToLocalStorage()
|
||||
}
|
||||
|
||||
const startEditingTitle = (item: JsonHistoryItem) => {
|
||||
editingItem.value = item
|
||||
savingTitle.value = item.title
|
||||
isSaveModalOpen.value = true
|
||||
}
|
||||
|
||||
const toggleFavorite = (id: string) => {
|
||||
const item = historyItems.value.find(item => item.id === id)
|
||||
if (item) {
|
||||
item.isFavorite = !item.isFavorite
|
||||
saveHistoryToLocalStorage()
|
||||
}
|
||||
}
|
||||
|
||||
const formatTimestamp = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleString()
|
||||
}
|
||||
|
||||
// 简化的JSON树形视图组件
|
||||
const JsonTreeView = ({ data }: { data: any }) => {
|
||||
// 这里应该是一个简化的树形视图实现
|
||||
// 为了保持代码简洁,我们暂时返回普通的JSON字符串
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.json-viewer {
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid;
|
||||
border-color: rgba(var(--color-primary), 0.15);
|
||||
background-color: rgba(var(--color-bg-secondary), 0.5);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
border-color: rgba(var(--color-primary), 0.4);
|
||||
background-color: rgba(var(--color-bg-secondary), 0.8);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user