forked from zguiy/utils
		
	
		
			
				
	
	
		
			847 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			847 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<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>  |