158 lines
4.5 KiB
Vue
158 lines
4.5 KiB
Vue
<!-- 命名转换:把输入串切分为词元,实时生成 camelCase / PascalCase / snake_case / kebab-case 等 6 种风格 -->
|
||
<template>
|
||
<div class="page-wrap">
|
||
<h2 class="page-title">命名转换</h2>
|
||
<p class="page-desc">输入变量名或短语,实时转换为 camelCase、PascalCase、snake_case、kebab-case 等 6 种常见命名风格。</p>
|
||
|
||
<t-card class="tool-section" title="输入">
|
||
<t-input
|
||
v-model="input"
|
||
placeholder="输入变量名,例如 my_user_name 或 myUserName"
|
||
clearable
|
||
@enter="saveHistory"
|
||
/>
|
||
</t-card>
|
||
|
||
<ErrorAlert :message="error" @close="error = ''" />
|
||
|
||
<t-card v-if="words.length" class="tool-section" title="转换结果">
|
||
<div class="convert-list">
|
||
<div v-for="row in resultRows" :key="row.name" class="convert-row">
|
||
<span class="convert-label">{{ row.name }}</span>
|
||
<code class="convert-value">{{ row.value }}</code>
|
||
<CopyButton :text="row.value" label="复制" />
|
||
</div>
|
||
</div>
|
||
</t-card>
|
||
|
||
<HistoryPanel
|
||
:items="history.items"
|
||
@select="onHistorySelect"
|
||
@remove="history.remove"
|
||
@clear="history.clear"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref } from 'vue'
|
||
import ErrorAlert from '@/components/common/ErrorAlert.vue'
|
||
import CopyButton from '@/components/common/CopyButton.vue'
|
||
import HistoryPanel from '@/components/common/HistoryPanel.vue'
|
||
import { useHistory } from '@/composables/useHistory'
|
||
|
||
const history = useHistory('case-convert')
|
||
|
||
const input = ref('')
|
||
// 本页为实时转换、无校验失败场景,error 仅用于保持全站页面结构一致
|
||
const error = ref('')
|
||
|
||
/**
|
||
* 将输入切分为全小写词元数组:
|
||
* 先按非字母数字切分(下划线/连字符/空格等作为分隔符),中文每个汉字视为独立词元;
|
||
* 再把连续字母数字串按 camelCase 边界(小写/数字→大写、字母→数字、数字→字母)细分。
|
||
*/
|
||
function tokenize(s: string): string[] {
|
||
const words: string[] = []
|
||
const segments = s.match(/[a-zA-Z0-9]+|[\u4e00-\u9fa5]/g) ?? []
|
||
for (const seg of segments) {
|
||
if (/^[\u4e00-\u9fa5]$/.test(seg)) {
|
||
words.push(seg)
|
||
continue
|
||
}
|
||
// 在驼峰边界插入空格再切分,最后统一转小写
|
||
const parts = seg
|
||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||
.replace(/([A-Za-z])(\d)/g, '$1 $2')
|
||
.replace(/(\d)([A-Za-z])/g, '$1 $2')
|
||
.split(/\s+/)
|
||
.map((w) => w.toLowerCase())
|
||
words.push(...parts)
|
||
}
|
||
return words
|
||
}
|
||
|
||
const words = computed(() => tokenize(input.value))
|
||
|
||
/** 单词首字母大写(对中文词元无副作用) */
|
||
function capitalize(w: string): string {
|
||
return w.charAt(0).toUpperCase() + w.slice(1)
|
||
}
|
||
|
||
/** 6 种命名风格的实时输出 */
|
||
const outputs = computed(() => {
|
||
const ws = words.value
|
||
return {
|
||
camel: ws.map((w, i) => (i === 0 ? w : capitalize(w))).join(''),
|
||
pascal: ws.map(capitalize).join(''),
|
||
snake: ws.join('_'),
|
||
upperSnake: ws.join('_').toUpperCase(),
|
||
kebab: ws.join('-'),
|
||
space: ws.join(' '),
|
||
}
|
||
})
|
||
|
||
/** 供模板遍历的展示行(风格名固定宽度放左侧) */
|
||
const resultRows = computed(() => [
|
||
{ name: 'camelCase', value: outputs.value.camel },
|
||
{ name: 'PascalCase', value: outputs.value.pascal },
|
||
{ name: 'snake_case', value: outputs.value.snake },
|
||
{ name: 'UPPER_SNAKE_CASE', value: outputs.value.upperSnake },
|
||
{ name: 'kebab-case', value: outputs.value.kebab },
|
||
{ name: '空格分隔', value: outputs.value.space },
|
||
])
|
||
|
||
/** 回车把当前输入存入历史 */
|
||
function saveHistory() {
|
||
if (input.value.trim()) history.add(input.value)
|
||
}
|
||
|
||
function onHistorySelect(text: string) {
|
||
input.value = text
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.convert-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.convert-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--td-border-level-1-color);
|
||
}
|
||
.convert-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.convert-label {
|
||
width: 150px;
|
||
flex-shrink: 0;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--td-text-color-secondary);
|
||
}
|
||
.convert-value {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 6px 10px;
|
||
border-radius: 6px;
|
||
background: var(--td-bg-color-secondarycontainer);
|
||
font-family: monospace;
|
||
font-size: 13px;
|
||
color: var(--td-text-color-primary);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
@media (max-width: 767px) {
|
||
/* 窄屏下风格名与内容上下排列,避免横向滚动 */
|
||
.convert-row {
|
||
flex-wrap: wrap;
|
||
}
|
||
.convert-label {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style>
|