资源整理

This commit is contained in:
tianmo committed 2026-10-30 19:10:29 +08:00
1 parent 7062a60c73
commit 5efee6d918
37 files changed
+265 -29

No files matched your search

+9 -9
View File
@@ -10,15 +10,15 @@ import zhCN from './locales/zh-CN.json'
import zhTW from './locales/zh-TW.json'
export const SUPPORTED_LOCALES = [
{ code: 'zh-CN', label: '中文' },
{ code: 'zh-TW', label: '繁體中文' },
{ code: 'en', label: 'English' },
{ code: 'ja', label: '日本語' },
{ code: 'ko', label: '한국어' },
{ code: 'fr', label: 'Français' },
{ code: 'de', label: 'Deutsch' },
{ code: 'pt-BR', label: 'Português' },
{ code: 'es', label: 'Español' },
{ code: 'zh-CN', label: '中文', flag: 'cn', shortLabel: '中' },
{ code: 'zh-TW', label: '繁體中文', flag: 'cn', shortLabel: '繁' },
{ code: 'en', label: 'English', flag: 'us', shortLabel: 'EN' },
{ code: 'ja', label: '日本語', flag: 'jp', shortLabel: 'JA' },
{ code: 'ko', label: '한국어', flag: 'kr', shortLabel: 'KO' },
{ code: 'fr', label: 'Français', flag: 'fr', shortLabel: 'FR' },
{ code: 'de', label: 'Deutsch', flag: 'de', shortLabel: 'DE' },
{ code: 'pt-BR', label: 'Português (Brasil)', flag: 'br', shortLabel: 'PT' },
{ code: 'es', label: 'Español', flag: 'es', shortLabel: 'ES' },
] as const
export type AppLocale = typeof SUPPORTED_LOCALES[number]['code']
+133 -13
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useParticleStore } from '../store/particleStore'
import { zoomIn, zoomOut, fitView, resetView, toggleGrid, type StageContext } from './stage/viewControls'
@@ -27,6 +27,12 @@ const { t, locale } = useI18n()
const showBackgroundMenu = ref(false)
const backgroundFileInput = ref<HTMLInputElement | null>(null)
const backgroundFileName = computed(() => store.settings.backgroundImageName)
const showLanguageMenu = ref(false)
const languagePicker = ref<HTMLElement | null>(null)
const languageTrigger = ref<HTMLButtonElement | null>(null)
const languageOptions = ref<HTMLButtonElement[]>([])
const currentLocale = computed(() => SUPPORTED_LOCALES.find((entry) => entry.code === locale.value) ?? SUPPORTED_LOCALES[0])
const flagSpriteUrl = `${import.meta.env.BASE_URL}images/flags.svg`
function toggleTool(t: '' | 'translate' | 'rotate' | 'scale') {
emit('update:activeTool', props.activeTool === t ? '' : t)
@@ -61,10 +67,55 @@ function undo() {
undoLastOperation()
}
function changeLanguage(event: Event) {
const next = (event.target as HTMLSelectElement).value as AppLocale
setAppLocale(next)
function focusLanguageOption(index: number) {
const options = languageOptions.value
options[index]?.focus()
}
function openLanguageMenu() {
showLanguageMenu.value = true
nextTick(() => {
const index = SUPPORTED_LOCALES.findIndex((entry) => entry.code === locale.value)
focusLanguageOption(Math.max(0, index))
})
}
function closeLanguageMenu(refocus = false) {
showLanguageMenu.value = false
if (refocus) nextTick(() => languageTrigger.value?.focus())
}
function toggleLanguageMenu() {
if (showLanguageMenu.value) closeLanguageMenu()
else openLanguageMenu()
}
function selectLanguage(next: AppLocale) {
setAppLocale(next)
closeLanguageMenu(true)
}
function onLanguageTriggerKeydown(event: KeyboardEvent) {
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return
event.preventDefault()
openLanguageMenu()
}
function onLanguageOptionKeydown(event: KeyboardEvent, index: number) {
if (event.key === 'Escape') {
event.preventDefault()
closeLanguageMenu(true)
return
}
let nextIndex = index
if (event.key === 'ArrowDown') nextIndex = (index + 1) % SUPPORTED_LOCALES.length
else if (event.key === 'ArrowUp') nextIndex = (index - 1 + SUPPORTED_LOCALES.length) % SUPPORTED_LOCALES.length
else if (event.key === 'Home') nextIndex = 0
else if (event.key === 'End') nextIndex = SUPPORTED_LOCALES.length - 1
else return
event.preventDefault()
focusLanguageOption(nextIndex)
}
function onDocumentPointerDown(event: PointerEvent) {
if (showLanguageMenu.value && !languagePicker.value?.contains(event.target as Node)) closeLanguageMenu()
}
onMounted(() => document.addEventListener('pointerdown', onDocumentPointerDown))
onUnmounted(() => document.removeEventListener('pointerdown', onDocumentPointerDown))
</script>
<template>
@@ -122,12 +173,57 @@ function changeLanguage(event: Event) {
</div>
</div>
<label class="language-picker">
<div ref="languagePicker" class="language-picker" @keydown.esc.prevent="closeLanguageMenu(true)">
<span>{{ t('language.label') }}</span>
<select :value="locale" :aria-label="t('language.label')" @change="changeLanguage">
<option v-for="entry in SUPPORTED_LOCALES" :key="entry.code" :value="entry.code">{{ entry.label }}</option>
</select>
</label>
<div class="language-dropdown">
<button
ref="languageTrigger"
type="button"
class="language-trigger"
:class="{ open: showLanguageMenu }"
:aria-label="t('language.label')"
aria-haspopup="listbox"
:aria-expanded="showLanguageMenu"
@click="toggleLanguageMenu"
@keydown.down.prevent="onLanguageTriggerKeydown"
@keydown.up.prevent="onLanguageTriggerKeydown"
>
<svg class="language-flag-icon" viewBox="0 0 24 16" aria-hidden="true">
<use :href="`${flagSpriteUrl}#${currentLocale.flag}`" />
</svg>
<span class="language-short">{{ currentLocale.shortLabel }}</span>
<svg class="language-chevron" viewBox="0 0 10 6" aria-hidden="true">
<path d="m1 1 4 4 4-4" />
</svg>
</button>
<div
v-if="showLanguageMenu"
id="language-menu"
class="language-menu"
role="listbox"
:aria-label="t('language.label')"
>
<button
v-for="(entry, index) in SUPPORTED_LOCALES"
:key="entry.code"
ref="languageOptions"
type="button"
class="language-option"
:class="{ selected: entry.code === locale }"
role="option"
:aria-selected="entry.code === locale"
@click="selectLanguage(entry.code)"
@keydown="onLanguageOptionKeydown($event, index)"
>
<svg class="language-option-flag-icon" viewBox="0 0 24 16" aria-hidden="true">
<use :href="`${flagSpriteUrl}#${entry.flag}`" />
</svg>
<span>{{ entry.label }}</span>
<span v-if="entry.code === locale" class="language-check" aria-hidden="true">✓</span>
</button>
</div>
</div>
</div>
<button class="help-btn" :class="{ on: showHelp }" :title="t('toolbar.openHelp')" @click="openHelp">{{ t('toolbar.help') }}</button>
<span class="zoom-pct">{{ Math.round(store.editor.viewScale * 100) }}%</span>
</div>
@@ -220,11 +316,35 @@ b { color: #ffcc33; }
display: flex; height: 28px; align-items: center; gap: 6px; margin-left: auto;
color: #9aa8c1; font-size: 11px; white-space: nowrap;
}
.language-picker select {
height: 28px; padding: 0 24px 0 8px; border: 1px solid #3a4963; border-radius: 6px;
outline: none; background: #202a3b; color: #dbe4f5; font-size: 11px; cursor: pointer;
.language-dropdown { position: relative; }
.language-trigger {
display: flex; height: 28px; min-width: 88px; align-items: center; gap: 7px; padding: 0 8px;
border: 1px solid #3a4963; border-radius: 6px; outline: none;
background: #202a3b; color: #dbe4f5; font: inherit; font-size: 11px; cursor: pointer;
}
.language-picker select:focus { border-color: #7774ff; }
.language-trigger:hover, .language-trigger.open { border-color: #5b608c; background: #2b3a5c; }
.language-trigger:focus-visible { border-color: #7774ff; }
.language-flag-icon, .language-option-flag-icon {
display: block; width: 22px; height: 15px; flex: 0 0 auto; overflow: hidden;
border-radius: 2px; box-shadow: 0 0 0 1px rgba(255,255,255,.12);
}
.language-short { min-width: 20px; color: #dbe4f5; font-size: 11px; font-weight: 600; }
.language-chevron { width: 10px; height: 6px; margin-left: auto; fill: none; stroke: #aab; stroke-width: 1.7; transition: transform .15s ease; }
.language-trigger.open .language-chevron { transform: rotate(180deg); }
.language-menu {
position: absolute; right: 0; bottom: calc(100% + 6px); z-index: 30;
width: 190px; max-height: min(360px, calc(100vh - 120px)); overflow-y: auto;
padding: 5px; border: 1px solid #343b50; border-radius: 9px;
background: rgba(20,20,32,.98); box-shadow: 0 8px 24px rgba(0,0,0,.48);
}
.language-option {
display: grid; width: 100%; min-height: 38px; grid-template-columns: 24px 1fr 16px; align-items: center;
gap: 8px; padding: 0 7px; border: 0; border-radius: 6px;
background: transparent; color: #aeb8cc; font: inherit; font-size: 12px; text-align: left; white-space: nowrap; cursor: pointer;
}
.language-option:hover, .language-option:focus-visible { outline: none; background: #2b3a5c; color: #fff; }
.language-option.selected { background: #343b63; color: #fff; }
.language-check { justify-self: end; color: #fff; font-size: 18px; font-weight: 500; }
.help-btn {
height: 28px; padding: 0 11px; flex: 0 0 auto; border: 1px solid #3a4963; border-radius: 6px;
background: #202a3b; color: #cbd7ed; font-size: 12px; font-weight: 700; cursor: pointer;