镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-21 06:01:51 +08:00
feat(popup): 将语言选择器替换为更美观的下拉菜单
重构语言选择器为现代风格的下拉菜单,改进用户体验 添加菜单动画效果和键盘导航支持 优化无障碍访问特性
这个提交包含在:
父节点
16dbfca817
当前提交
8583808dd3
@ -21,12 +21,29 @@
|
||||
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.platform-status {
|
||||
@ -106,43 +123,105 @@
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 语言选择器样式 */
|
||||
.language-selector {
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
/* Header actions */
|
||||
.header-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.language-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
.menu-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ddd;
|
||||
.icon-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.language-select:hover {
|
||||
border-color: #1890ff;
|
||||
.icon-button:hover {
|
||||
background: rgba(24, 144, 255, 0.08);
|
||||
border-color: rgba(24, 144, 255, 0.35);
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.language-select:focus {
|
||||
.icon-button:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
min-width: 160px;
|
||||
padding: 6px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.menu-item::after {
|
||||
content: '';
|
||||
width: 16px;
|
||||
text-align: right;
|
||||
color: #1890ff;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.menu-item:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.menu-item[data-selected='true'] {
|
||||
background: rgba(24, 144, 255, 0.08);
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.menu-item[data-selected='true']::after {
|
||||
content: '✓';
|
||||
}
|
||||
|
||||
/* Toast提示样式 */
|
||||
.toast {
|
||||
position: fixed;
|
||||
@ -283,16 +362,24 @@
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1 data-i18n="popup_title">CloudDrive Renamer</h1>
|
||||
</div>
|
||||
|
||||
<!-- 语言选择器 -->
|
||||
<div class="language-selector">
|
||||
<label class="language-label" data-i18n="popup_language_label">语言 / Language</label>
|
||||
<select class="language-select" id="language-select">
|
||||
<option value="zh_CN">简体中文</option>
|
||||
<option value="zh_TW">繁體中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
<div class="header-actions">
|
||||
<div class="menu-container">
|
||||
<button class="icon-button" id="language-button" type="button" aria-haspopup="menu" aria-expanded="false">
|
||||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M3 12h18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M12 3c3 3 3 15 0 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M12 3c-3 3-3 15 0 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
<span class="sr-only" data-i18n="popup_language_label">语言 / Language</span>
|
||||
</button>
|
||||
<div class="menu" id="language-menu" role="menu" aria-labelledby="language-button" hidden>
|
||||
<button class="menu-item" type="button" role="menuitemradio" aria-checked="false" data-lang="zh_CN">简体中文</button>
|
||||
<button class="menu-item" type="button" role="menuitemradio" aria-checked="false" data-lang="zh_TW">繁體中文</button>
|
||||
<button class="menu-item" type="button" role="menuitemradio" aria-checked="false" data-lang="en">English</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="platform-status">
|
||||
|
||||
@ -4,7 +4,8 @@ import { PlatformName } from '../types/platform';
|
||||
import { PlatformUsageStats, STORAGE_KEYS } from '../types/stats';
|
||||
import { APP_VERSION_WITH_PREFIX } from '../shared/version';
|
||||
import { I18nService } from '../utils/i18n';
|
||||
import { LanguageChangeMessage } from '../types/i18n';
|
||||
import { LANGUAGE_DISPLAY_NAMES } from '../types/i18n';
|
||||
import type { LanguageChangeMessage, SupportedLanguage } from '../types/i18n';
|
||||
import { detectPlatformFromUrl } from '../utils/platform-detector';
|
||||
|
||||
/**
|
||||
@ -46,6 +47,38 @@ function localizeHTML() {
|
||||
});
|
||||
}
|
||||
|
||||
function updateLanguageMenuSelection(language: SupportedLanguage) {
|
||||
const languageMenu = document.getElementById('language-menu');
|
||||
if (!languageMenu) return;
|
||||
|
||||
languageMenu.querySelectorAll<HTMLButtonElement>('[data-lang]').forEach((item) => {
|
||||
const isSelected = item.dataset.lang === language;
|
||||
item.setAttribute('aria-checked', isSelected ? 'true' : 'false');
|
||||
item.dataset.selected = isSelected ? 'true' : 'false';
|
||||
});
|
||||
}
|
||||
|
||||
function setLanguageMenuOpen(open: boolean, options?: { focusButton?: boolean }) {
|
||||
const languageButton = document.getElementById('language-button') as HTMLButtonElement | null;
|
||||
const languageMenu = document.getElementById('language-menu') as HTMLElement | null;
|
||||
if (!languageButton || !languageMenu) return;
|
||||
|
||||
languageMenu.hidden = !open;
|
||||
languageButton.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
|
||||
if (open) {
|
||||
const selectedItem =
|
||||
languageMenu.querySelector<HTMLButtonElement>('[data-selected="true"]') ||
|
||||
languageMenu.querySelector<HTMLButtonElement>('[data-lang]');
|
||||
selectedItem?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (options?.focusButton) {
|
||||
languageButton.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update platform display name based on current tab
|
||||
* Extracts duplicated logic and removes global state mutation
|
||||
@ -78,6 +111,16 @@ function handleLanguageChange(message: any) {
|
||||
// Update all internationalized text when language changes
|
||||
localizeHTML();
|
||||
|
||||
// Update language menu selection
|
||||
const newLanguage = (message as LanguageChangeMessage).language;
|
||||
updateLanguageMenuSelection(newLanguage);
|
||||
|
||||
// Keep tooltip up-to-date for icon-only button
|
||||
const languageButton = document.getElementById('language-button') as HTMLButtonElement | null;
|
||||
if (languageButton) {
|
||||
languageButton.title = I18nService.t('popup_language_label');
|
||||
}
|
||||
|
||||
// Update platform name with new translations
|
||||
updatePlatformDisplayName().catch(error => {
|
||||
logger.error('Failed to update platform name after language change:', error);
|
||||
@ -99,8 +142,8 @@ function setupLanguageChangeListener() {
|
||||
|
||||
// 初始化Popup
|
||||
async function initPopup() {
|
||||
// 1. First initialize language selector (loads and sets currentLanguage asynchronously)
|
||||
await initLanguageSelector();
|
||||
// 1. First initialize language menu (loads and sets currentLanguage asynchronously)
|
||||
await initLanguageMenu();
|
||||
|
||||
// 2. After language initialization, localize HTML with correct language
|
||||
localizeHTML();
|
||||
@ -315,42 +358,75 @@ function showToast(message: string, duration = 2000) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化语言选择器
|
||||
* 初始化语言菜单
|
||||
*/
|
||||
async function initLanguageSelector() {
|
||||
const languageSelect = document.getElementById('language-select') as HTMLSelectElement;
|
||||
if (!languageSelect) return;
|
||||
async function initLanguageMenu() {
|
||||
const languageButton = document.getElementById('language-button') as HTMLButtonElement | null;
|
||||
const languageMenu = document.getElementById('language-menu') as HTMLElement | null;
|
||||
if (!languageButton || !languageMenu) return;
|
||||
|
||||
const menuContainer = languageButton.closest('.menu-container');
|
||||
if (!menuContainer) return;
|
||||
|
||||
// 获取当前语言并设置选中状态
|
||||
const currentLang = await I18nService.getCurrentLanguage();
|
||||
languageSelect.value = currentLang;
|
||||
updateLanguageMenuSelection(currentLang);
|
||||
|
||||
// Icon-only button: add localized tooltip
|
||||
languageButton.title = I18nService.t('popup_language_label');
|
||||
|
||||
languageButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
setLanguageMenuOpen(languageMenu.hidden);
|
||||
});
|
||||
|
||||
// 监听语言切换
|
||||
languageSelect.addEventListener('change', async (e) => {
|
||||
const newLang = (e.target as HTMLSelectElement).value as 'zh_CN' | 'zh_TW' | 'en';
|
||||
languageMenu.querySelectorAll<HTMLButtonElement>('[data-lang]').forEach((item) => {
|
||||
item.addEventListener('click', async () => {
|
||||
const newLang = item.dataset.lang as SupportedLanguage;
|
||||
|
||||
try {
|
||||
// 设置新语言
|
||||
await I18nService.setLanguage(newLang);
|
||||
// Close menu immediately for better UX
|
||||
setLanguageMenuOpen(false, { focusButton: true });
|
||||
|
||||
// 更新页面所有文本
|
||||
localizeHTML();
|
||||
try {
|
||||
// 设置新语言
|
||||
await I18nService.setLanguage(newLang);
|
||||
|
||||
// 显示Toast提示
|
||||
const langName = {
|
||||
'zh_CN': '简体中文',
|
||||
'zh_TW': '繁體中文',
|
||||
'en': 'English'
|
||||
}[newLang];
|
||||
// 更新页面所有文本
|
||||
localizeHTML();
|
||||
|
||||
showToast(I18nService.t('toast_language_changed', [langName]));
|
||||
// 更新 tooltip
|
||||
languageButton.title = I18nService.t('popup_language_label');
|
||||
|
||||
// 更新平台名称翻译(使用提取的函数,消除重复代码)
|
||||
await updatePlatformDisplayName();
|
||||
} catch (error) {
|
||||
logger.error('Failed to change language:', error as Error);
|
||||
showToast(I18nService.t('toast_language_change_failed'));
|
||||
}
|
||||
// 更新菜单选中状态
|
||||
updateLanguageMenuSelection(newLang);
|
||||
|
||||
// 显示Toast提示
|
||||
showToast(I18nService.t('toast_language_changed', [LANGUAGE_DISPLAY_NAMES[newLang]]));
|
||||
|
||||
// 更新平台名称翻译(使用提取的函数,消除重复代码)
|
||||
await updatePlatformDisplayName();
|
||||
} catch (error) {
|
||||
logger.error('Failed to change language:', error as Error);
|
||||
showToast(I18nService.t('toast_language_change_failed'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 点击其他区域关闭菜单
|
||||
document.addEventListener('click', (e) => {
|
||||
if (languageMenu.hidden) return;
|
||||
const target = e.target as Node | null;
|
||||
if (target && menuContainer.contains(target)) return;
|
||||
setLanguageMenuOpen(false);
|
||||
});
|
||||
|
||||
// ESC 关闭菜单
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (languageMenu.hidden) return;
|
||||
e.preventDefault();
|
||||
setLanguageMenuOpen(false, { focusButton: true });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
* 1. Edit version in package.json
|
||||
* 2. Run: npm run sync:version (or automatically via prebuild/predev)
|
||||
*
|
||||
* Generated at: 2025-12-30T05:36:20.170Z
|
||||
* Generated at: 2025-12-30T06:54:19.507Z
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户