1
0
forked from zguiy/utils

工具完成

This commit is contained in:
2025-06-28 22:38:49 +08:00
parent 2c668fedd0
commit 8400dbfab9
60 changed files with 23197 additions and 144 deletions

View 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>