Files
utils/src/components/tools/CodeFormatter.vue
zguiy 8400dbfab9
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
工具完成
2025-06-28 22:38:49 +08:00

565 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="space-y-6">
<!-- 工具栏 -->
<div 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>