565 lines
16 KiB
Vue
565 lines
16 KiB
Vue
<template>
|
||
<div class="space-y-6">
|
||
<!-- 工具栏 -->
|
||
<div class="card p-4">
|
||
<div class="flex flex-wrap gap-2">
|
||
<button
|
||
@click="formatCode"
|
||
:disabled="!inputCode.trim() || isFormatting"
|
||
class="btn-primary"
|
||
>
|
||
<FontAwesomeIcon
|
||
:icon="isFormatting ? ['fas', 'spinner'] : ['fas', 'code']"
|
||
:class="['mr-2', isFormatting && 'animate-spin']"
|
||
/>
|
||
{{ t('tools.code_formatter.format') }}
|
||
</button>
|
||
|
||
<button
|
||
@click="minifyCode"
|
||
:disabled="!inputCode.trim() || isFormatting"
|
||
class="btn-secondary"
|
||
>
|
||
<FontAwesomeIcon :icon="['fas', 'compress']" class="mr-2" />
|
||
{{ t('tools.code_formatter.minify') }}
|
||
</button>
|
||
|
||
<button
|
||
@click="copyToClipboard"
|
||
:disabled="!outputCode"
|
||
class="btn-secondary"
|
||
>
|
||
<FontAwesomeIcon
|
||
:icon="copied ? ['fas', 'check'] : ['fas', 'copy']"
|
||
:class="['mr-2', copied && 'text-success']"
|
||
/>
|
||
{{ t('tools.code_formatter.copy') }}
|
||
</button>
|
||
|
||
<button
|
||
@click="clearAll"
|
||
class="btn-secondary"
|
||
>
|
||
<FontAwesomeIcon :icon="['fas', 'trash']" class="mr-2" />
|
||
{{ t('tools.code_formatter.clear') }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 语言选择和设置 -->
|
||
<div class="card p-4">
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">
|
||
{{ t('tools.code_formatter.language') }}
|
||
</label>
|
||
<select v-model="selectedLanguage" class="select-field" @change="handleLanguageChange">
|
||
<option v-for="lang in supportedLanguages" :key="lang.value" :value="lang.value">
|
||
{{ lang.label }}
|
||
</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">
|
||
{{ t('tools.code_formatter.indent_size') }}
|
||
</label>
|
||
<select v-model="indentSize" class="select-field">
|
||
<option value="2">2 空格</option>
|
||
<option value="4">4 空格</option>
|
||
<option value="tab">制表符</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-secondary mb-2">
|
||
{{ t('tools.code_formatter.line_width') }}
|
||
</label>
|
||
<input
|
||
v-model="lineWidth"
|
||
type="number"
|
||
min="80"
|
||
max="200"
|
||
class="input-field"
|
||
placeholder="120"
|
||
>
|
||
</div>
|
||
</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">
|
||
<h3 class="text-lg font-semibold text-primary">{{ t('tools.code_formatter.input') }}</h3>
|
||
<div class="flex space-x-2">
|
||
<button
|
||
@click="pasteFromClipboard"
|
||
class="p-2 rounded text-secondary hover:text-primary transition-colors"
|
||
title="粘贴"
|
||
>
|
||
<FontAwesomeIcon :icon="['fas', 'clipboard']" />
|
||
</button>
|
||
<button
|
||
@click="loadExample"
|
||
class="p-2 rounded text-secondary hover:text-primary transition-colors"
|
||
title="加载示例"
|
||
>
|
||
<FontAwesomeIcon :icon="['fas', 'file-code']" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<textarea
|
||
v-model="inputCode"
|
||
:placeholder="getPlaceholder()"
|
||
class="textarea-field font-mono text-sm"
|
||
style="height: 500px; resize: vertical;"
|
||
@input="handleInputChange"
|
||
/>
|
||
|
||
<div class="text-sm text-secondary mt-2">
|
||
行数: {{ inputLines }} | 字符数: {{ inputCode.length }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 输出区域 -->
|
||
<div class="card p-4">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="text-lg font-semibold text-primary">{{ t('tools.code_formatter.output') }}</h3>
|
||
<div class="text-sm text-secondary">
|
||
{{ outputStats }}
|
||
</div>
|
||
</div>
|
||
|
||
<textarea
|
||
v-model="outputCode"
|
||
:placeholder="t('tools.code_formatter.output_placeholder')"
|
||
class="textarea-field font-mono text-sm"
|
||
style="height: 500px; resize: vertical;"
|
||
readonly
|
||
/>
|
||
|
||
<div v-if="outputCode" class="text-sm text-secondary mt-2">
|
||
行数: {{ outputLines }} | 字符数: {{ outputCode.length }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 状态消息 -->
|
||
<div v-if="statusMessage" class="card p-4">
|
||
<div :class="[
|
||
'flex items-center space-x-2',
|
||
statusType === 'success' ? 'text-success' : 'text-error'
|
||
]">
|
||
<FontAwesomeIcon
|
||
:icon="statusType === 'success' ? ['fas', 'check'] : ['fas', 'times']"
|
||
/>
|
||
<span>{{ statusMessage }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import { useLanguage } from '@/composables/useLanguage'
|
||
|
||
const { t } = useLanguage()
|
||
|
||
// 响应式状态
|
||
const inputCode = ref('')
|
||
const outputCode = ref('')
|
||
const selectedLanguage = ref('javascript')
|
||
const indentSize = ref('2')
|
||
const lineWidth = ref(120)
|
||
const isFormatting = ref(false)
|
||
const copied = ref(false)
|
||
const statusMessage = ref('')
|
||
const statusType = ref<'success' | 'error'>('success')
|
||
|
||
// 支持的语言
|
||
const supportedLanguages = [
|
||
{ value: 'javascript', label: 'JavaScript' },
|
||
{ value: 'typescript', label: 'TypeScript' },
|
||
{ value: 'html', label: 'HTML' },
|
||
{ value: 'css', label: 'CSS' },
|
||
{ value: 'json', label: 'JSON' },
|
||
{ value: 'sql', label: 'SQL' },
|
||
{ value: 'xml', label: 'XML' },
|
||
{ value: 'yaml', label: 'YAML' },
|
||
{ value: 'markdown', label: 'Markdown' }
|
||
]
|
||
|
||
// 计算属性
|
||
const inputLines = computed(() => {
|
||
return inputCode.value.split('\n').length
|
||
})
|
||
|
||
const outputLines = computed(() => {
|
||
return outputCode.value.split('\n').length
|
||
})
|
||
|
||
const outputStats = computed(() => {
|
||
if (!outputCode.value) return ''
|
||
const originalSize = inputCode.value.length
|
||
const formattedSize = outputCode.value.length
|
||
const diff = formattedSize - originalSize
|
||
const diffText = diff > 0 ? `+${diff}` : diff.toString()
|
||
return `${diffText} 字符`
|
||
})
|
||
|
||
// 格式化代码
|
||
const formatCode = async () => {
|
||
if (!inputCode.value.trim()) {
|
||
showStatus('请输入要格式化的代码', 'error')
|
||
return
|
||
}
|
||
|
||
isFormatting.value = true
|
||
statusMessage.value = ''
|
||
|
||
try {
|
||
let formatted = ''
|
||
|
||
switch (selectedLanguage.value) {
|
||
case 'json':
|
||
formatted = formatJSON(inputCode.value)
|
||
break
|
||
case 'html':
|
||
formatted = formatHTML(inputCode.value)
|
||
break
|
||
case 'css':
|
||
formatted = formatCSS(inputCode.value)
|
||
break
|
||
case 'javascript':
|
||
case 'typescript':
|
||
formatted = formatJavaScript(inputCode.value)
|
||
break
|
||
case 'sql':
|
||
formatted = formatSQL(inputCode.value)
|
||
break
|
||
case 'xml':
|
||
formatted = formatXML(inputCode.value)
|
||
break
|
||
default:
|
||
formatted = formatGeneric(inputCode.value)
|
||
}
|
||
|
||
outputCode.value = formatted
|
||
showStatus('代码格式化成功', 'success')
|
||
|
||
} catch (error) {
|
||
console.error('格式化失败:', error)
|
||
showStatus('格式化失败: ' + (error instanceof Error ? error.message : '未知错误'), 'error')
|
||
} finally {
|
||
isFormatting.value = false
|
||
}
|
||
}
|
||
|
||
// 压缩代码
|
||
const minifyCode = () => {
|
||
if (!inputCode.value.trim()) {
|
||
showStatus('请输入要压缩的代码', 'error')
|
||
return
|
||
}
|
||
|
||
try {
|
||
let minified = ''
|
||
|
||
switch (selectedLanguage.value) {
|
||
case 'json':
|
||
const parsed = JSON.parse(inputCode.value)
|
||
minified = JSON.stringify(parsed)
|
||
break
|
||
case 'css':
|
||
minified = minifyCSS(inputCode.value)
|
||
break
|
||
case 'javascript':
|
||
minified = minifyJavaScript(inputCode.value)
|
||
break
|
||
default:
|
||
minified = inputCode.value.replace(/\s+/g, ' ').trim()
|
||
}
|
||
|
||
outputCode.value = minified
|
||
showStatus('代码压缩成功', 'success')
|
||
|
||
} catch (error) {
|
||
console.error('压缩失败:', error)
|
||
showStatus('压缩失败: ' + (error instanceof Error ? error.message : '未知错误'), 'error')
|
||
}
|
||
}
|
||
|
||
// JSON格式化
|
||
const formatJSON = (code: string): string => {
|
||
const parsed = JSON.parse(code)
|
||
const indent = indentSize.value === 'tab' ? '\t' : ' '.repeat(parseInt(indentSize.value))
|
||
return JSON.stringify(parsed, null, indent)
|
||
}
|
||
|
||
// HTML格式化
|
||
const formatHTML = (code: string): string => {
|
||
const indent = indentSize.value === 'tab' ? '\t' : ' '.repeat(parseInt(indentSize.value))
|
||
let level = 0
|
||
let formatted = ''
|
||
|
||
// 简单的HTML格式化逻辑
|
||
const lines = code.replace(/></g, '>\n<').split('\n')
|
||
|
||
for (let line of lines) {
|
||
line = line.trim()
|
||
if (!line) continue
|
||
|
||
if (line.startsWith('</')) {
|
||
level = Math.max(0, level - 1)
|
||
}
|
||
|
||
formatted += indent.repeat(level) + line + '\n'
|
||
|
||
if (line.startsWith('<') && !line.startsWith('</') && !line.endsWith('/>') && !line.includes('</')) {
|
||
level++
|
||
}
|
||
}
|
||
|
||
return formatted.trim()
|
||
}
|
||
|
||
// CSS格式化
|
||
const formatCSS = (code: string): string => {
|
||
const indent = indentSize.value === 'tab' ? '\t' : ' '.repeat(parseInt(indentSize.value))
|
||
|
||
return code
|
||
.replace(/\s*{\s*/g, ' {\n')
|
||
.replace(/;\s*/g, ';\n')
|
||
.replace(/\s*}\s*/g, '\n}\n')
|
||
.split('\n')
|
||
.map(line => {
|
||
line = line.trim()
|
||
if (!line) return ''
|
||
if (line.endsWith('{') || line.endsWith('}')) {
|
||
return line
|
||
}
|
||
return indent + line
|
||
})
|
||
.filter(line => line !== '')
|
||
.join('\n')
|
||
}
|
||
|
||
// JavaScript格式化
|
||
const formatJavaScript = (code: string): string => {
|
||
const indent = indentSize.value === 'tab' ? '\t' : ' '.repeat(parseInt(indentSize.value))
|
||
let level = 0
|
||
let formatted = ''
|
||
let inString = false
|
||
let stringChar = ''
|
||
|
||
for (let i = 0; i < code.length; i++) {
|
||
const char = code[i]
|
||
const prevChar = code[i - 1]
|
||
|
||
if ((char === '"' || char === "'") && prevChar !== '\\') {
|
||
if (!inString) {
|
||
inString = true
|
||
stringChar = char
|
||
} else if (char === stringChar) {
|
||
inString = false
|
||
stringChar = ''
|
||
}
|
||
}
|
||
|
||
if (!inString) {
|
||
if (char === '{') {
|
||
formatted += char + '\n' + indent.repeat(++level)
|
||
continue
|
||
} else if (char === '}') {
|
||
formatted = formatted.trimEnd() + '\n' + indent.repeat(--level) + char
|
||
if (code[i + 1] && code[i + 1] !== ';' && code[i + 1] !== ',' && code[i + 1] !== ')') {
|
||
formatted += '\n' + indent.repeat(level)
|
||
}
|
||
continue
|
||
} else if (char === ';') {
|
||
formatted += char
|
||
if (code[i + 1] && code[i + 1] !== '}') {
|
||
formatted += '\n' + indent.repeat(level)
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
|
||
formatted += char
|
||
}
|
||
|
||
return formatted.trim()
|
||
}
|
||
|
||
// SQL格式化
|
||
const formatSQL = (code: string): string => {
|
||
const keywords = ['SELECT', 'FROM', 'WHERE', 'JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'ORDER BY', 'GROUP BY', 'HAVING', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP']
|
||
const indent = indentSize.value === 'tab' ? '\t' : ' '.repeat(parseInt(indentSize.value))
|
||
|
||
let formatted = code.toUpperCase()
|
||
|
||
keywords.forEach(keyword => {
|
||
const regex = new RegExp(`\\b${keyword}\\b`, 'gi')
|
||
formatted = formatted.replace(regex, `\n${keyword}`)
|
||
})
|
||
|
||
return formatted
|
||
.split('\n')
|
||
.map(line => line.trim())
|
||
.filter(line => line !== '')
|
||
.join('\n')
|
||
}
|
||
|
||
// XML格式化
|
||
const formatXML = (code: string): string => {
|
||
// 简单的XML格式化,复用HTML格式化逻辑
|
||
return formatHTML(code)
|
||
}
|
||
|
||
// 通用格式化
|
||
const formatGeneric = (code: string): string => {
|
||
// 基本的缩进处理
|
||
const indent = indentSize.value === 'tab' ? '\t' : ' '.repeat(parseInt(indentSize.value))
|
||
let level = 0
|
||
|
||
return code.split('\n').map(line => {
|
||
line = line.trim()
|
||
if (!line) return ''
|
||
|
||
// 简单的括号缩进
|
||
const openBrackets = (line.match(/[{(\[]/g) || []).length
|
||
const closeBrackets = (line.match(/[})\]]/g) || []).length
|
||
|
||
if (closeBrackets > openBrackets) {
|
||
level = Math.max(0, level - (closeBrackets - openBrackets))
|
||
}
|
||
|
||
const formatted = indent.repeat(level) + line
|
||
|
||
if (openBrackets > closeBrackets) {
|
||
level += (openBrackets - closeBrackets)
|
||
}
|
||
|
||
return formatted
|
||
}).join('\n')
|
||
}
|
||
|
||
// CSS压缩
|
||
const minifyCSS = (code: string): string => {
|
||
return code
|
||
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除注释
|
||
.replace(/\s+/g, ' ') // 多个空格替换为单个
|
||
.replace(/;\s*}/g, '}') // 移除最后一个分号
|
||
.replace(/\s*{\s*/g, '{')
|
||
.replace(/;\s*/g, ';')
|
||
.replace(/:\s*/g, ':')
|
||
.trim()
|
||
}
|
||
|
||
// JavaScript压缩
|
||
const minifyJavaScript = (code: string): string => {
|
||
return code
|
||
.replace(/\/\/.*$/gm, '') // 移除单行注释
|
||
.replace(/\/\*[\s\S]*?\*\//g, '') // 移除多行注释
|
||
.replace(/\s+/g, ' ') // 多个空格替换为单个
|
||
.replace(/\s*([{}();,])\s*/g, '$1') // 移除操作符周围的空格
|
||
.trim()
|
||
}
|
||
|
||
// 复制到剪贴板
|
||
const copyToClipboard = async () => {
|
||
if (!outputCode.value) return
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(outputCode.value)
|
||
copied.value = true
|
||
showStatus('代码已复制到剪贴板', 'success')
|
||
setTimeout(() => {
|
||
copied.value = false
|
||
}, 2000)
|
||
} catch (error) {
|
||
console.error('复制失败:', error)
|
||
showStatus('复制失败', 'error')
|
||
}
|
||
}
|
||
|
||
// 从剪贴板粘贴
|
||
const pasteFromClipboard = async () => {
|
||
try {
|
||
const text = await navigator.clipboard.readText()
|
||
inputCode.value = text
|
||
handleInputChange()
|
||
} catch (error) {
|
||
console.error('粘贴失败:', error)
|
||
showStatus('粘贴失败', 'error')
|
||
}
|
||
}
|
||
|
||
// 加载示例代码
|
||
const loadExample = () => {
|
||
const examples: Record<string, string> = {
|
||
javascript: `function calculateSum(numbers) {
|
||
const result = numbers.reduce((sum, num) => {
|
||
return sum + num;
|
||
}, 0);
|
||
return result;
|
||
}
|
||
|
||
const data = [1, 2, 3, 4, 5];
|
||
console.log(calculateSum(data));`,
|
||
|
||
json: `{"name":"极速箱工具集","version":"1.0.0","tools":[{"id":1,"name":"代码格式化器","active":true},{"id":2,"name":"JSON格式化器","active":true}],"settings":{"theme":"dark","language":"zh-CN"}}`,
|
||
|
||
html: `<div class="container"><header><h1>标题</h1></header><main><section><p>这是一段文本</p></section></main></div>`,
|
||
|
||
css: `.container{display:flex;flex-direction:column;}.header{background-color:#333;color:white;padding:20px;}.main{flex:1;padding:20px;}`,
|
||
|
||
sql: `SELECT u.name, u.email, p.title FROM users u INNER JOIN posts p ON u.id = p.user_id WHERE u.active = 1 ORDER BY p.created_at DESC;`
|
||
}
|
||
|
||
inputCode.value = examples[selectedLanguage.value] || examples.javascript
|
||
}
|
||
|
||
// 清除所有内容
|
||
const clearAll = () => {
|
||
inputCode.value = ''
|
||
outputCode.value = ''
|
||
statusMessage.value = ''
|
||
}
|
||
|
||
// 处理输入变化
|
||
const handleInputChange = () => {
|
||
if (statusMessage.value) {
|
||
statusMessage.value = ''
|
||
}
|
||
}
|
||
|
||
// 处理语言变化
|
||
const handleLanguageChange = () => {
|
||
outputCode.value = ''
|
||
}
|
||
|
||
// 获取占位符文本
|
||
const getPlaceholder = (): string => {
|
||
const placeholders: Record<string, string> = {
|
||
javascript: '输入 JavaScript 代码...',
|
||
html: '输入 HTML 代码...',
|
||
css: '输入 CSS 代码...',
|
||
json: '输入 JSON 数据...',
|
||
sql: '输入 SQL 语句...'
|
||
}
|
||
|
||
return placeholders[selectedLanguage.value] || '输入代码...'
|
||
}
|
||
|
||
// 显示状态消息
|
||
const showStatus = (message: string, type: 'success' | 'error') => {
|
||
statusMessage.value = message
|
||
statusType.value = type
|
||
setTimeout(() => {
|
||
statusMessage.value = ''
|
||
}, 3000)
|
||
}
|
||
</script> |