工具完成
This commit is contained in:
565
src/components/tools/CodeFormatter.vue
Normal file
565
src/components/tools/CodeFormatter.vue
Normal file
@ -0,0 +1,565 @@
|
||||
<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>
|
Reference in New Issue
Block a user