1
0
forked from zguiy/utils
Files
utils/src/components/tools/JsonFormatter.vue
2025-06-28 22:38:49 +08:00

847 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="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>