🚧 Optimize
This commit is contained in:
@@ -1,20 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from 'vue';
|
||||
import {onBeforeMount} from 'vue';
|
||||
import {useConfigStore} from '@/stores/configStore';
|
||||
import {useSystemStore} from '@/stores/systemStore';
|
||||
import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||
import {useThemeStore} from '@/stores/themeStore';
|
||||
import {useUpdateStore} from '@/stores/updateStore';
|
||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||
import {useTranslationStore} from "@/stores/translationStore";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const systemStore = useSystemStore();
|
||||
const keybindingStore = useKeybindingStore();
|
||||
const themeStore = useThemeStore();
|
||||
const updateStore = useUpdateStore();
|
||||
const translationStore = useTranslationStore();
|
||||
|
||||
// 应用启动时加载配置和初始化系统信息
|
||||
onMounted(async () => {
|
||||
onBeforeMount(async () => {
|
||||
// 并行初始化配置、系统信息和快捷键配置
|
||||
await Promise.all([
|
||||
configStore.initConfig(),
|
||||
@@ -24,7 +25,8 @@ onMounted(async () => {
|
||||
|
||||
// 初始化语言和主题
|
||||
await configStore.initializeLanguage();
|
||||
themeStore.initializeTheme();
|
||||
await themeStore.initializeTheme();
|
||||
await translationStore.loadTranslators();
|
||||
|
||||
// 启动时检查更新
|
||||
await updateStore.checkOnStartup();
|
||||
|
||||
49
frontend/src/common/constant/translation.ts
Normal file
49
frontend/src/common/constant/translation.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 默认翻译配置
|
||||
*/
|
||||
export const DEFAULT_TRANSLATION_CONFIG = {
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 翻译相关的错误消息
|
||||
*/
|
||||
export const TRANSLATION_ERRORS = {
|
||||
NO_TEXT: 'no text to translate',
|
||||
TRANSLATION_FAILED: 'translation failed',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 翻译结果接口
|
||||
*/
|
||||
export interface TranslationResult {
|
||||
translatedText: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言信息接口
|
||||
*/
|
||||
export interface LanguageInfo {
|
||||
Code: string; // 语言代码
|
||||
Name: string; // 语言名称
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译器扩展配置
|
||||
*/
|
||||
export interface TranslatorConfig {
|
||||
/** 最小选择字符数才显示翻译按钮 */
|
||||
minSelectionLength: number;
|
||||
/** 最大翻译字符数 */
|
||||
maxTranslationLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译图标SVG
|
||||
*/
|
||||
export const TRANSLATION_ICON_SVG = `
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
|
||||
</svg>`;
|
||||
@@ -1,11 +1,11 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, reactive} from 'vue';
|
||||
import {SystemThemeType, ThemeType, ThemeColorConfig} from '@/../bindings/voidraft/internal/models/models';
|
||||
import {ThemeService} from '@/../bindings/voidraft/internal/services';
|
||||
import {useConfigStore} from './configStore';
|
||||
import {useEditorStore} from './editorStore';
|
||||
import {defaultDarkColors} from '@/views/editor/theme/dark';
|
||||
import {defaultLightColors} from '@/views/editor/theme/light';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, reactive } from 'vue';
|
||||
import { SystemThemeType, ThemeType, ThemeColorConfig } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { ThemeService } from '@/../bindings/voidraft/internal/services';
|
||||
import { useConfigStore } from './configStore';
|
||||
import { useEditorStore } from './editorStore';
|
||||
import { defaultDarkColors } from '@/views/editor/theme/dark';
|
||||
import { defaultLightColors } from '@/views/editor/theme/light';
|
||||
|
||||
/**
|
||||
* 主题管理 Store
|
||||
@@ -14,25 +14,44 @@ import {defaultLightColors} from '@/views/editor/theme/light';
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
const configStore = useConfigStore();
|
||||
|
||||
// 响应式状态 - 存储当前使用的主题颜色
|
||||
// 响应式状态
|
||||
const themeColors = reactive({
|
||||
darkTheme: { ...defaultDarkColors },
|
||||
lightTheme: { ...defaultLightColors }
|
||||
});
|
||||
|
||||
// 计算属性 - 当前选择的主题类型
|
||||
// 计算属性
|
||||
const currentTheme = computed(() =>
|
||||
configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto
|
||||
);
|
||||
|
||||
// 初始化主题颜色 - 从数据库加载
|
||||
// 获取默认主题颜色
|
||||
const getDefaultColors = (themeType: ThemeType) =>
|
||||
themeType === ThemeType.ThemeTypeDark ? defaultDarkColors : defaultLightColors;
|
||||
|
||||
// 应用主题到 DOM
|
||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||
const themeMap = {
|
||||
[SystemThemeType.SystemThemeAuto]: 'auto',
|
||||
[SystemThemeType.SystemThemeDark]: 'dark',
|
||||
[SystemThemeType.SystemThemeLight]: 'light'
|
||||
};
|
||||
document.documentElement.setAttribute('data-theme', themeMap[theme]);
|
||||
};
|
||||
|
||||
// 初始化主题颜色
|
||||
const initializeThemeColors = async () => {
|
||||
try {
|
||||
const themes = await ThemeService.GetDefaultThemes();
|
||||
|
||||
// 如果没有获取到主题数据,使用默认值
|
||||
if (!themes) {
|
||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新主题颜色
|
||||
if (themes[ThemeType.ThemeTypeDark]) {
|
||||
Object.assign(themeColors.darkTheme, themes[ThemeType.ThemeTypeDark].colors);
|
||||
}
|
||||
@@ -47,17 +66,9 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 应用主题到 DOM
|
||||
const applyThemeToDOM = (theme: SystemThemeType) => {
|
||||
document.documentElement.setAttribute('data-theme',
|
||||
theme === SystemThemeType.SystemThemeAuto ? 'auto' :
|
||||
theme === SystemThemeType.SystemThemeDark ? 'dark' : 'light'
|
||||
);
|
||||
};
|
||||
|
||||
// 初始化主题
|
||||
const initializeTheme = async () => {
|
||||
const theme = configStore.config?.appearance?.systemTheme || SystemThemeType.SystemThemeAuto;
|
||||
const theme = currentTheme.value;
|
||||
applyThemeToDOM(theme);
|
||||
await initializeThemeColors();
|
||||
};
|
||||
@@ -69,27 +80,25 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
refreshEditorTheme();
|
||||
};
|
||||
|
||||
// 更新主题颜色
|
||||
const updateThemeColors = (darkColors: any = null, lightColors: any = null): boolean => {
|
||||
// 更新主题颜色 - 合并逻辑,减少重复代码
|
||||
const updateThemeColors = (darkColors?: any, lightColors?: any): boolean => {
|
||||
let hasChanges = false;
|
||||
|
||||
if (darkColors) {
|
||||
Object.entries(darkColors).forEach(([key, value]) => {
|
||||
if (value !== undefined && themeColors.darkTheme[key] !== value) {
|
||||
themeColors.darkTheme[key] = value;
|
||||
hasChanges = true;
|
||||
const updateColors = (target: any, source: any) => {
|
||||
if (!source) return false;
|
||||
|
||||
let changed = false;
|
||||
Object.entries(source).forEach(([key, value]) => {
|
||||
if (value !== undefined && target[key] !== value) {
|
||||
target[key] = value;
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
if (lightColors) {
|
||||
Object.entries(lightColors).forEach(([key, value]) => {
|
||||
if (value !== undefined && themeColors.lightTheme[key] !== value) {
|
||||
themeColors.lightTheme[key] = value;
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
hasChanges = updateColors(themeColors.darkTheme, darkColors) || hasChanges;
|
||||
hasChanges = updateColors(themeColors.lightTheme, lightColors) || hasChanges;
|
||||
|
||||
return hasChanges;
|
||||
};
|
||||
@@ -100,8 +109,10 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
const darkColors = ThemeColorConfig.createFrom(themeColors.darkTheme);
|
||||
const lightColors = ThemeColorConfig.createFrom(themeColors.lightTheme);
|
||||
|
||||
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeDark, darkColors);
|
||||
await ThemeService.UpdateThemeColors(ThemeType.ThemeTypeLight, lightColors);
|
||||
await Promise.all([
|
||||
ThemeService.UpdateThemeColors(ThemeType.ThemeTypeDark, darkColors),
|
||||
ThemeService.UpdateThemeColors(ThemeType.ThemeTypeLight, lightColors)
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Failed to save theme colors:', error);
|
||||
throw error;
|
||||
@@ -117,11 +128,8 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
await ThemeService.ResetThemeColors(dbThemeType);
|
||||
|
||||
// 2. 更新内存中的颜色状态
|
||||
if (themeType === 'darkTheme') {
|
||||
Object.assign(themeColors.darkTheme, defaultDarkColors);
|
||||
} else {
|
||||
Object.assign(themeColors.lightTheme, defaultLightColors);
|
||||
}
|
||||
const defaultColors = getDefaultColors(dbThemeType);
|
||||
Object.assign(themeColors[themeType], defaultColors);
|
||||
|
||||
// 3. 刷新编辑器主题
|
||||
refreshEditorTheme();
|
||||
@@ -136,13 +144,10 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
// 刷新编辑器主题
|
||||
const refreshEditorTheme = () => {
|
||||
// 使用当前主题重新应用DOM主题
|
||||
const theme = currentTheme.value;
|
||||
applyThemeToDOM(theme);
|
||||
applyThemeToDOM(currentTheme.value);
|
||||
|
||||
const editorStore = useEditorStore();
|
||||
if (editorStore) {
|
||||
editorStore.applyThemeSettings();
|
||||
}
|
||||
editorStore?.applyThemeSettings();
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,593 +1,117 @@
|
||||
import {defineStore} from 'pinia';
|
||||
import {computed, ref, watch} from 'vue';
|
||||
import {ref} from 'vue';
|
||||
import {TranslationService} from '@/../bindings/voidraft/internal/services';
|
||||
import {franc} from 'franc-min';
|
||||
import {LanguageInfo, TRANSLATION_ERRORS, TranslationResult} from '@/common/constant/translation';
|
||||
|
||||
export interface TranslationResult {
|
||||
sourceText: string;
|
||||
translatedText: string;
|
||||
sourceLang: string;
|
||||
targetLang: string;
|
||||
translatorType: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ISO 639-3 到 ISO 639-1/2 语言代码的映射
|
||||
* franc-min 返回的是 ISO 639-3 代码,需要转换为翻译API常用的 ISO 639-1/2 代码
|
||||
*/
|
||||
const ISO_LANGUAGE_MAP: Record<string, string> = {
|
||||
// 常见语言
|
||||
'cmn': 'zh', // 中文 (Mandarin Chinese)
|
||||
'eng': 'en', // 英文 (English)
|
||||
'jpn': 'ja', // 日语 (Japanese)
|
||||
'kor': 'ko', // 韩语 (Korean)
|
||||
'fra': 'fr', // 法语 (French)
|
||||
'deu': 'de', // 德语 (German)
|
||||
'spa': 'es', // 西班牙语 (Spanish)
|
||||
'rus': 'ru', // 俄语 (Russian)
|
||||
'ita': 'it', // 意大利语 (Italian)
|
||||
'nld': 'nl', // 荷兰语 (Dutch)
|
||||
'por': 'pt', // 葡萄牙语 (Portuguese)
|
||||
'vie': 'vi', // 越南语 (Vietnamese)
|
||||
'arb': 'ar', // 阿拉伯语 (Arabic)
|
||||
'hin': 'hi', // 印地语 (Hindi)
|
||||
'ben': 'bn', // 孟加拉语 (Bengali)
|
||||
'tha': 'th', // 泰语 (Thai)
|
||||
'tur': 'tr', // 土耳其语 (Turkish)
|
||||
'heb': 'he', // 希伯来语 (Hebrew)
|
||||
'pol': 'pl', // 波兰语 (Polish)
|
||||
'swe': 'sv', // 瑞典语 (Swedish)
|
||||
'fin': 'fi', // 芬兰语 (Finnish)
|
||||
'dan': 'da', // 丹麦语 (Danish)
|
||||
'ron': 'ro', // 罗马尼亚语 (Romanian)
|
||||
'hun': 'hu', // 匈牙利语 (Hungarian)
|
||||
'ces': 'cs', // 捷克语 (Czech)
|
||||
'ell': 'el', // 希腊语 (Greek)
|
||||
'bul': 'bg', // 保加利亚语 (Bulgarian)
|
||||
'cat': 'ca', // 加泰罗尼亚语 (Catalan)
|
||||
'ukr': 'uk', // 乌克兰语 (Ukrainian)
|
||||
'hrv': 'hr', // 克罗地亚语 (Croatian)
|
||||
'ind': 'id', // 印尼语 (Indonesian)
|
||||
'mal': 'ms', // 马来语 (Malay)
|
||||
'nob': 'no', // 挪威语 (Norwegian)
|
||||
'lat': 'la', // 拉丁语 (Latin)
|
||||
'lit': 'lt', // 立陶宛语 (Lithuanian)
|
||||
'slk': 'sk', // 斯洛伐克语 (Slovak)
|
||||
'slv': 'sl', // 斯洛文尼亚语 (Slovenian)
|
||||
'srp': 'sr', // 塞尔维亚语 (Serbian)
|
||||
'est': 'et', // 爱沙尼亚语 (Estonian)
|
||||
'lav': 'lv', // 拉脱维亚语 (Latvian)
|
||||
'fil': 'tl', // 菲律宾语/他加禄语 (Filipino/Tagalog)
|
||||
|
||||
// 未知/不确定
|
||||
'und': 'auto' // 未知语言
|
||||
};
|
||||
|
||||
// 语言代码的通用映射关系,适用于大部分翻译器
|
||||
const COMMON_LANGUAGE_ALIASES: Record<string, string[]> = {
|
||||
'zh': ['zh-CN', 'zh-TW', 'zh-Hans', 'zh-Hant', 'chinese', 'zhong'],
|
||||
'en': ['en-US', 'en-GB', 'english', 'eng'],
|
||||
'ja': ['jp', 'jpn', 'japanese'],
|
||||
'ko': ['kr', 'kor', 'korean'],
|
||||
'fr': ['fra', 'french'],
|
||||
'de': ['deu', 'german', 'ger'],
|
||||
'es': ['spa', 'spanish', 'esp'],
|
||||
'ru': ['rus', 'russian'],
|
||||
'pt': ['por', 'portuguese'],
|
||||
'it': ['ita', 'italian'],
|
||||
'nl': ['nld', 'dutch'],
|
||||
'ar': ['ara', 'arabic'],
|
||||
'hi': ['hin', 'hindi'],
|
||||
'th': ['tha', 'thai'],
|
||||
'tr': ['tur', 'turkish'],
|
||||
'vi': ['vie', 'vietnamese'],
|
||||
'id': ['ind', 'indonesian'],
|
||||
'ms': ['mal', 'malay'],
|
||||
'fi': ['fin', 'finnish'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译存储
|
||||
*/
|
||||
export const useTranslationStore = defineStore('translation', () => {
|
||||
// 状态
|
||||
const availableTranslators = ref<string[]>([]);
|
||||
const isTranslating = ref(false);
|
||||
const lastResult = ref<TranslationResult | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
// 语言列表 - 将类型设置为any以避免类型错误
|
||||
const languageMaps = ref<Record<string, Record<string, any>>>({});
|
||||
|
||||
// 语言使用频率计数 - 使用pinia持久化
|
||||
const languageUsageCount = ref<Record<string, number>>({});
|
||||
// 最近使用的翻译语言 - 最多记录10个
|
||||
const recentLanguages = ref<string[]>([]);
|
||||
|
||||
// 默认配置
|
||||
// 注意:确保默认值在初始化和持久化后正确设置
|
||||
const defaultTargetLang = ref('zh');
|
||||
const defaultTranslator = ref('bing');
|
||||
// 检测到的源语言,初始为空字符串表示尚未检测
|
||||
const detectedSourceLang = ref('');
|
||||
|
||||
// 计算属性
|
||||
const hasTranslators = computed(() => availableTranslators.value.length > 0);
|
||||
const currentLanguageMap = computed(() => {
|
||||
return languageMaps.value[defaultTranslator.value] || {};
|
||||
});
|
||||
|
||||
// 监听默认语言变更,确保目标语言在当前翻译器支持的范围内
|
||||
watch([defaultTranslator], () => {
|
||||
// 当切换翻译器时,验证默认目标语言是否支持
|
||||
if (Object.keys(languageMaps.value).length > 0) {
|
||||
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
|
||||
if (validatedLang !== defaultTargetLang.value) {
|
||||
console.log(`目标语言 ${defaultTargetLang.value} 不受支持,已切换到 ${validatedLang}`);
|
||||
defaultTargetLang.value = validatedLang;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 基础状态
|
||||
const translators = ref<string[]>([]);
|
||||
const isTranslating = ref<boolean>(false);
|
||||
// 语言映射
|
||||
const translatorLanguages = ref<Record<string, Record<string, LanguageInfo>>>({});
|
||||
|
||||
/**
|
||||
* 加载可用翻译器
|
||||
* 加载可用翻译器列表并预先加载所有语言映射
|
||||
*/
|
||||
const loadAvailableTranslators = async (): Promise<void> => {
|
||||
const loadTranslators = async (): Promise<void> => {
|
||||
try {
|
||||
const translators = await TranslationService.GetAvailableTranslators();
|
||||
availableTranslators.value = translators;
|
||||
|
||||
// 如果默认翻译器不在可用列表中,则使用第一个可用的翻译器
|
||||
if (translators.length > 0 && !translators.includes(defaultTranslator.value)) {
|
||||
defaultTranslator.value = translators[0];
|
||||
}
|
||||
|
||||
// 加载所有翻译器的语言列表
|
||||
await Promise.all(translators.map(loadTranslatorLanguages));
|
||||
|
||||
// 在加载完所有语言列表后,确保默认目标语言有效
|
||||
if (defaultTargetLang.value) {
|
||||
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
|
||||
if (validatedLang !== defaultTargetLang.value) {
|
||||
defaultTargetLang.value = validatedLang;
|
||||
translators.value = await TranslationService.GetTranslators();
|
||||
|
||||
const loadPromises = translators.value.map(async (translatorType) => {
|
||||
try {
|
||||
const languages = await TranslationService.GetTranslatorLanguages(translatorType as any);
|
||||
if (languages) {
|
||||
translatorLanguages.value[translatorType] = languages;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to preload languages for ${translatorType}:`, err);
|
||||
}
|
||||
}
|
||||
} catch (_err) {
|
||||
error.value = 'no available translators';
|
||||
});
|
||||
|
||||
// 等待所有语言映射加载完成
|
||||
await Promise.all(loadPromises);
|
||||
} catch (err) {
|
||||
console.error('Failed to load available translators:', err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载指定翻译器的语言列表
|
||||
* @param translatorType 翻译器类型
|
||||
*/
|
||||
const loadTranslatorLanguages = async (translatorType: string): Promise<void> => {
|
||||
try {
|
||||
const languages = await TranslationService.GetTranslatorLanguages(translatorType as any);
|
||||
|
||||
if (languages) {
|
||||
languageMaps.value[translatorType] = languages;
|
||||
translatorLanguages.value[translatorType] = languages;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to load languages for ${translatorType}:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测文本语言
|
||||
* @param text 待检测的文本
|
||||
* @returns 检测到的语言代码,如未检测到则返回空字符串
|
||||
*/
|
||||
const detectLanguage = (text: string): string => {
|
||||
if (!text || text.trim().length < 10) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
// franc返回ISO 639-3代码
|
||||
const detectedIso639_3 = franc(text);
|
||||
|
||||
// 如果是未知语言,返回空字符串
|
||||
if (detectedIso639_3 === 'und') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 转换为常用语言代码
|
||||
return ISO_LANGUAGE_MAP[detectedIso639_3] || '';
|
||||
} catch (err) {
|
||||
console.error('语言检测失败:', err);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 在翻译器语言列表中查找相似的语言代码
|
||||
* @param langCode 待查找的语言代码
|
||||
* @param translatorType 翻译器类型
|
||||
* @returns 找到的语言代码或空字符串
|
||||
*/
|
||||
const findSimilarLanguage = (langCode: string, translatorType: string): string => {
|
||||
if (!langCode) return '';
|
||||
|
||||
const languageMap = languageMaps.value[translatorType] || {};
|
||||
const langCodeLower = langCode.toLowerCase();
|
||||
|
||||
// 1. 尝试精确匹配
|
||||
if (languageMap[langCode]) {
|
||||
return langCode;
|
||||
}
|
||||
|
||||
// 2. 检查通用别名映射
|
||||
const possibleAliases = Object.entries(COMMON_LANGUAGE_ALIASES).find(
|
||||
([code, aliases]) => code === langCodeLower || aliases.includes(langCodeLower)
|
||||
);
|
||||
|
||||
if (possibleAliases) {
|
||||
// 检查主代码是否可用
|
||||
const [mainCode, aliases] = possibleAliases;
|
||||
if (languageMap[mainCode]) {
|
||||
return mainCode;
|
||||
}
|
||||
|
||||
// 检查别名是否可用
|
||||
for (const alias of aliases) {
|
||||
if (languageMap[alias]) {
|
||||
return alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 尝试正则表达式匹配
|
||||
// 创建一个基于语言代码的正则表达式:例如 'en' 会匹配 'en-US', 'en_GB' 等
|
||||
const codePattern = new RegExp(`^${langCodeLower}[-_]?`, 'i');
|
||||
|
||||
// 在语言列表中查找匹配的语言代码
|
||||
const availableCodes = Object.keys(languageMap);
|
||||
const matchedCode = availableCodes.find(code =>
|
||||
codePattern.test(code.toLowerCase())
|
||||
);
|
||||
|
||||
if (matchedCode) {
|
||||
return matchedCode;
|
||||
}
|
||||
|
||||
// 4. 反向匹配,例如 'zh-CN' 应该能匹配到 'zh'
|
||||
if (langCodeLower.includes('-') || langCodeLower.includes('_')) {
|
||||
const baseCode = langCodeLower.split(/[-_]/)[0];
|
||||
if (languageMap[baseCode]) {
|
||||
return baseCode;
|
||||
}
|
||||
|
||||
// 通过基础代码查找别名
|
||||
const baseCodeAliases = Object.entries(COMMON_LANGUAGE_ALIASES).find(
|
||||
([code, aliases]) => code === baseCode || aliases.includes(baseCode)
|
||||
);
|
||||
|
||||
if (baseCodeAliases) {
|
||||
const [mainCode, aliases] = baseCodeAliases;
|
||||
if (languageMap[mainCode]) {
|
||||
return mainCode;
|
||||
}
|
||||
|
||||
for (const alias of aliases) {
|
||||
if (languageMap[alias]) {
|
||||
return alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 最后尝试查找与部分代码匹配的任何语言
|
||||
const partialMatch = availableCodes.find(code =>
|
||||
code.toLowerCase().includes(langCodeLower) ||
|
||||
langCodeLower.includes(code.toLowerCase())
|
||||
);
|
||||
|
||||
if (partialMatch) {
|
||||
return partialMatch;
|
||||
}
|
||||
|
||||
// 如果所有匹配都失败,返回英语作为默认值
|
||||
return 'en';
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证语言代码是否受当前翻译器支持
|
||||
* @param langCode 语言代码
|
||||
* @param translatorType 翻译器类型(可选,默认使用当前翻译器)
|
||||
* @returns 验证后的语言代码
|
||||
*/
|
||||
const validateLanguage = (langCode: string, translatorType?: string): string => {
|
||||
// 如果语言代码为空,返回auto作为API调用的默认值
|
||||
if (!langCode) return 'auto';
|
||||
|
||||
const currentType = translatorType || defaultTranslator.value;
|
||||
|
||||
// 尝试在指定翻译器的语言列表中查找相似的语言代码
|
||||
return findSimilarLanguage(langCode, currentType) || 'auto';
|
||||
};
|
||||
|
||||
/**
|
||||
* 增加语言使用次数并添加到最近使用列表
|
||||
* @param langCode 语言代码
|
||||
* @param weight 权重,默认为1
|
||||
*/
|
||||
const incrementLanguageUsage = (langCode: string, weight: number = 1): void => {
|
||||
if (!langCode || langCode === 'auto') return;
|
||||
|
||||
// 转换为小写,确保一致性
|
||||
const normalizedCode = langCode.toLowerCase();
|
||||
|
||||
// 更新使用次数,乘以权重
|
||||
const currentCount = languageUsageCount.value[normalizedCode] || 0;
|
||||
languageUsageCount.value[normalizedCode] = currentCount + weight;
|
||||
|
||||
// 更新最近使用的语言列表
|
||||
updateRecentLanguages(normalizedCode);
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新最近使用的语言列表
|
||||
* @param langCode 语言代码
|
||||
*/
|
||||
const updateRecentLanguages = (langCode: string): void => {
|
||||
if (!langCode) return;
|
||||
|
||||
// 如果已经在列表中,先移除它
|
||||
const index = recentLanguages.value.indexOf(langCode);
|
||||
if (index !== -1) {
|
||||
recentLanguages.value.splice(index, 1);
|
||||
}
|
||||
|
||||
// 添加到列表开头
|
||||
recentLanguages.value.unshift(langCode);
|
||||
|
||||
// 保持列表最多10个元素
|
||||
if (recentLanguages.value.length > 10) {
|
||||
recentLanguages.value = recentLanguages.value.slice(0, 10);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取按使用频率排序的语言列表
|
||||
* @param translatorType 翻译器类型
|
||||
* @param grouped 是否分组返回(常用/其他)
|
||||
* @returns 排序后的语言列表或分组后的语言列表
|
||||
*/
|
||||
const getSortedLanguages = (translatorType: string, grouped: boolean = false): [string, any][] | {frequent: [string, any][], others: [string, any][]} => {
|
||||
const languageMap = languageMaps.value[translatorType] || {};
|
||||
|
||||
// 获取语言列表
|
||||
const languages = Object.entries(languageMap);
|
||||
|
||||
// 按使用频率排序
|
||||
const sortedLanguages = languages.sort(([codeA, infoA], [codeB, infoB]) => {
|
||||
// 获取使用次数,默认为0
|
||||
const countA = languageUsageCount.value[codeA.toLowerCase()] || 0;
|
||||
const countB = languageUsageCount.value[codeB.toLowerCase()] || 0;
|
||||
|
||||
// 首先按使用频率降序排序
|
||||
if (countB !== countA) {
|
||||
return countB - countA;
|
||||
}
|
||||
|
||||
// 其次按最近使用情况排序
|
||||
const recentIndexA = recentLanguages.value.indexOf(codeA.toLowerCase());
|
||||
const recentIndexB = recentLanguages.value.indexOf(codeB.toLowerCase());
|
||||
|
||||
if (recentIndexA !== -1 && recentIndexB !== -1) {
|
||||
return recentIndexA - recentIndexB;
|
||||
} else if (recentIndexA !== -1) {
|
||||
return -1;
|
||||
} else if (recentIndexB !== -1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 如果使用频率和最近使用情况都相同,按名称排序
|
||||
const nameA = infoA.Name || infoA.name || codeA;
|
||||
const nameB = infoB.Name || infoB.name || codeB;
|
||||
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
// 如果不需要分组,直接返回排序后的列表
|
||||
if (!grouped) {
|
||||
return sortedLanguages;
|
||||
}
|
||||
|
||||
// 分组:将有使用记录的语言归为常用组,其他归为其他组
|
||||
const frequentLanguages: [string, any][] = [];
|
||||
const otherLanguages: [string, any][] = [];
|
||||
|
||||
sortedLanguages.forEach(lang => {
|
||||
const [code] = lang;
|
||||
const usageCount = languageUsageCount.value[code.toLowerCase()] || 0;
|
||||
const isInRecent = recentLanguages.value.includes(code.toLowerCase());
|
||||
|
||||
if (usageCount > 0 || isInRecent) {
|
||||
frequentLanguages.push(lang);
|
||||
} else {
|
||||
otherLanguages.push(lang);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
frequent: frequentLanguages,
|
||||
others: otherLanguages
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译文本
|
||||
* @param text 待翻译文本
|
||||
* @param to 目标语言代码
|
||||
* @param translatorType 翻译器类型
|
||||
* @returns 翻译结果
|
||||
*/
|
||||
const translateText = async (
|
||||
text: string,
|
||||
to?: string,
|
||||
translatorType?: string
|
||||
text: string,
|
||||
sourceLang: string,
|
||||
targetLang: string,
|
||||
translatorType: string
|
||||
): Promise<TranslationResult> => {
|
||||
// 使用提供的参数或默认值
|
||||
const targetLang = to || defaultTargetLang.value;
|
||||
const translator = translatorType || defaultTranslator.value;
|
||||
|
||||
// 处理空文本
|
||||
if (!text) {
|
||||
|
||||
if (!text.trim()) {
|
||||
return {
|
||||
sourceText: '',
|
||||
translatedText: '',
|
||||
sourceLang: '',
|
||||
targetLang: targetLang,
|
||||
translatorType: translator,
|
||||
error: 'no text to translate'
|
||||
error: TRANSLATION_ERRORS.NO_TEXT
|
||||
};
|
||||
}
|
||||
|
||||
// 检测源语言
|
||||
const detected = detectLanguage(text);
|
||||
if (detected) {
|
||||
detectedSourceLang.value = detected;
|
||||
}
|
||||
|
||||
// 使用检测到的语言或回退到auto
|
||||
let actualSourceLang = detectedSourceLang.value || 'auto';
|
||||
// 特殊处理有道翻译器:有道翻译器允许源语言和目标语言都是auto
|
||||
const isYoudaoTranslator = translatorType === 'youdao';
|
||||
const bothAuto = sourceLang === 'auto' && targetLang === 'auto';
|
||||
|
||||
// 确认语言代码有效并针对当前翻译器进行匹配
|
||||
actualSourceLang = validateLanguage(actualSourceLang, translator);
|
||||
const actualTargetLang = validateLanguage(targetLang, translator);
|
||||
|
||||
// 如果源语言和目标语言相同,则直接返回原文
|
||||
if (actualSourceLang !== 'auto' && actualSourceLang === actualTargetLang) {
|
||||
if (sourceLang === targetLang && !(isYoudaoTranslator && bothAuto)) {
|
||||
return {
|
||||
sourceText: text,
|
||||
translatedText: text,
|
||||
sourceLang: actualSourceLang,
|
||||
targetLang: actualTargetLang,
|
||||
translatorType: translator
|
||||
translatedText: text
|
||||
};
|
||||
}
|
||||
|
||||
isTranslating.value = true;
|
||||
error.value = null;
|
||||
|
||||
|
||||
try {
|
||||
console.log(`翻译文本: 从 ${actualSourceLang} 到 ${actualTargetLang} 使用 ${translator} 翻译器`);
|
||||
|
||||
// 调用翻译服务
|
||||
const translatedText = await TranslationService.TranslateWith(
|
||||
text,
|
||||
actualSourceLang,
|
||||
actualTargetLang,
|
||||
translator
|
||||
text,
|
||||
sourceLang,
|
||||
targetLang,
|
||||
translatorType
|
||||
);
|
||||
|
||||
// 增加目标语言的使用频率,使用较大的权重
|
||||
incrementLanguageUsage(actualTargetLang, 3);
|
||||
|
||||
// 如果源语言不是auto,也记录其使用情况,但权重较小
|
||||
if (actualSourceLang !== 'auto') {
|
||||
incrementLanguageUsage(actualSourceLang, 1);
|
||||
}
|
||||
|
||||
// 构建结果
|
||||
const result: TranslationResult = {
|
||||
sourceText: text,
|
||||
translatedText,
|
||||
sourceLang: actualSourceLang,
|
||||
targetLang: actualTargetLang,
|
||||
translatorType: translator
|
||||
|
||||
return {
|
||||
translatedText
|
||||
};
|
||||
|
||||
lastResult.value = result;
|
||||
return result;
|
||||
} catch (err) {
|
||||
// 处理错误
|
||||
const errorMessage = err instanceof Error ? err.message : 'translation failed';
|
||||
const errorMessage = err instanceof Error ? err.message : TRANSLATION_ERRORS.TRANSLATION_FAILED;
|
||||
|
||||
error.value = errorMessage;
|
||||
|
||||
const result: TranslationResult = {
|
||||
sourceText: text,
|
||||
return {
|
||||
translatedText: '',
|
||||
sourceLang: actualSourceLang,
|
||||
targetLang: actualTargetLang,
|
||||
translatorType: translator,
|
||||
error: errorMessage
|
||||
};
|
||||
|
||||
lastResult.value = result;
|
||||
return result;
|
||||
} finally {
|
||||
isTranslating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置默认翻译配置
|
||||
* @param config 配置对象
|
||||
*/
|
||||
const setDefaultConfig = (config: {
|
||||
targetLang?: string;
|
||||
translatorType?: string;
|
||||
}): void => {
|
||||
let changed = false;
|
||||
|
||||
if (config.translatorType && config.translatorType !== defaultTranslator.value) {
|
||||
defaultTranslator.value = config.translatorType;
|
||||
// 切换翻译器时清空检测到的源语言,以便重新检测
|
||||
detectedSourceLang.value = '';
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (config.targetLang) {
|
||||
// 验证目标语言是否支持
|
||||
const validLang = validateLanguage(config.targetLang, defaultTranslator.value);
|
||||
defaultTargetLang.value = validLang;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
console.log(`已更新默认翻译配置:翻译器=${defaultTranslator.value},目标语言=${defaultTargetLang.value}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化时加载可用翻译器
|
||||
loadAvailableTranslators();
|
||||
|
||||
return {
|
||||
// 状态
|
||||
availableTranslators,
|
||||
translators,
|
||||
isTranslating,
|
||||
lastResult,
|
||||
error,
|
||||
detectedSourceLang,
|
||||
defaultTargetLang,
|
||||
defaultTranslator,
|
||||
languageMaps,
|
||||
languageUsageCount,
|
||||
recentLanguages,
|
||||
|
||||
// 计算属性
|
||||
hasTranslators,
|
||||
currentLanguageMap,
|
||||
|
||||
translatorLanguages,
|
||||
|
||||
// 方法
|
||||
loadAvailableTranslators,
|
||||
loadTranslators,
|
||||
loadTranslatorLanguages,
|
||||
translateText,
|
||||
setDefaultConfig,
|
||||
detectLanguage,
|
||||
validateLanguage,
|
||||
findSimilarLanguage,
|
||||
getSortedLanguages,
|
||||
incrementLanguageUsage
|
||||
};
|
||||
}, {
|
||||
persist: {
|
||||
key: 'voidraft-translation',
|
||||
storage: localStorage,
|
||||
pick: ['languageUsageCount', 'defaultTargetLang', 'defaultTranslator', 'recentLanguages']
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,364 +1,355 @@
|
||||
import { Extension, StateField, StateEffect } from '@codemirror/state';
|
||||
import { Extension, StateField, StateEffect, StateEffectType } from '@codemirror/state';
|
||||
import { EditorView, showTooltip, Tooltip } from '@codemirror/view';
|
||||
import { createTranslationTooltip } from './tooltip';
|
||||
|
||||
/**
|
||||
* 翻译器扩展配置
|
||||
*/
|
||||
export interface TranslatorConfig {
|
||||
/** 默认翻译服务提供商 */
|
||||
defaultTranslator: string;
|
||||
/** 最小选择字符数才显示翻译按钮 */
|
||||
minSelectionLength: number;
|
||||
/** 最大翻译字符数 */
|
||||
maxTranslationLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认翻译器配置
|
||||
*/
|
||||
export const defaultConfig: TranslatorConfig = {
|
||||
defaultTranslator: 'bing',
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
};
|
||||
|
||||
// 全局配置存储
|
||||
let currentConfig: TranslatorConfig = {...defaultConfig};
|
||||
// 存储选择的文本用于翻译
|
||||
let selectedTextForTranslation = "";
|
||||
import {
|
||||
TranslatorConfig,
|
||||
DEFAULT_TRANSLATION_CONFIG,
|
||||
TRANSLATION_ICON_SVG
|
||||
} from '@/common/constant/translation';
|
||||
|
||||
|
||||
/**
|
||||
* 翻译图标SVG
|
||||
*/
|
||||
const translationIconSvg = `
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M599.68 485.056h-8l30.592 164.672c20.352-7.04 38.72-17.344 54.912-31.104a271.36 271.36 0 0 1-40.704-64.64l32.256-4.032c8.896 17.664 19.072 33.28 30.592 46.72 23.872-27.968 42.24-65.152 55.04-111.744l-154.688 0.128z m121.92 133.76c18.368 15.36 39.36 26.56 62.848 33.472l14.784 4.416-8.64 30.336-14.72-4.352a205.696 205.696 0 0 1-76.48-41.728c-20.672 17.92-44.928 31.552-71.232 40.064l20.736 110.912H519.424l-9.984 72.512h385.152c18.112 0 32.704-14.144 32.704-31.616V295.424a32.128 32.128 0 0 0-32.704-31.552H550.528l35.2 189.696h79.424v-31.552h61.44v31.552h102.4v31.616h-42.688c-14.272 55.488-35.712 100.096-64.64 133.568zM479.36 791.68H193.472c-36.224 0-65.472-28.288-65.472-63.168V191.168C128 156.16 157.312 128 193.472 128h327.68l20.544 104.32h352.832c36.224 0 65.472 28.224 65.472 63.104v537.408c0 34.944-29.312 63.168-65.472 63.168H468.608l10.688-104.32zM337.472 548.352v-33.28H272.768v-48.896h60.16V433.28h-60.16v-41.728h64.704v-32.896h-102.4v189.632h102.4z m158.272 0V453.76c0-17.216-4.032-30.272-12.16-39.488-8.192-9.152-20.288-13.696-36.032-13.696a55.04 55.04 0 0 0-24.768 5.376 39.04 39.04 0 0 0-17.088 15.936h-1.984l-5.056-18.56h-28.352V548.48h37.12V480c0-17.088 2.304-29.376 6.912-36.736 4.608-7.424 12.16-11.072 22.528-11.072 7.616 0 13.248 2.56 16.64 7.872 3.52 5.248 5.312 13.056 5.312 23.488v84.736h36.928z" fill="currentColor"></path>
|
||||
</svg>`;
|
||||
class TranslatorExtension {
|
||||
private config: TranslatorConfig;
|
||||
private setTranslationTooltip: StateEffectType<Tooltip | null>;
|
||||
private translationTooltipField: StateField<readonly Tooltip[]>;
|
||||
private translationButtonField: StateField<readonly Tooltip[]>;
|
||||
|
||||
// 用于设置翻译气泡的状态效果
|
||||
const setTranslationTooltip = StateEffect.define<Tooltip | null>();
|
||||
constructor(config?: Partial<TranslatorConfig>) {
|
||||
// 初始化配置
|
||||
this.config = {
|
||||
minSelectionLength: DEFAULT_TRANSLATION_CONFIG.minSelectionLength,
|
||||
maxTranslationLength: DEFAULT_TRANSLATION_CONFIG.maxTranslationLength,
|
||||
...config
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译气泡的状态字段
|
||||
*/
|
||||
const translationTooltipField = StateField.define<readonly Tooltip[]>({
|
||||
create() {
|
||||
return [];
|
||||
},
|
||||
update(tooltips, tr) {
|
||||
// 如果文档或选择变化,隐藏气泡
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 检查是否有特定的状态效果来更新tooltips
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(setTranslationTooltip)) {
|
||||
return effect.value ? [effect.value] : [];
|
||||
}
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
// 初始化状态效果
|
||||
this.setTranslationTooltip = StateEffect.define<Tooltip | null>();
|
||||
|
||||
|
||||
/**
|
||||
* 根据当前选择获取翻译按钮tooltip
|
||||
*/
|
||||
function getTranslationButtonTooltips(state: any): readonly Tooltip[] {
|
||||
|
||||
// 如果气泡已显示,则不显示按钮
|
||||
if (state.field(translationTooltipField).length > 0) return [];
|
||||
|
||||
const selection = state.selection.main;
|
||||
|
||||
// 如果没有选中文本,不显示按钮
|
||||
if (selection.empty) return [];
|
||||
|
||||
// 获取选中的文本
|
||||
const selectedText = state.sliceDoc(selection.from, selection.to);
|
||||
|
||||
// 检查文本是否只包含空格
|
||||
if (!selectedText.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 检查文本长度条件
|
||||
if (selectedText.length < currentConfig.minSelectionLength ||
|
||||
selectedText.length > currentConfig.maxTranslationLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 保存选中的文本用于翻译
|
||||
selectedTextForTranslation = selectedText;
|
||||
|
||||
// 返回翻译按钮tooltip配置
|
||||
return [{
|
||||
pos: selection.to,
|
||||
above: false,
|
||||
strictSide: true,
|
||||
arrow: false,
|
||||
create: (view) => {
|
||||
// 创建按钮DOM
|
||||
const dom = document.createElement('div');
|
||||
dom.className = 'cm-translator-button';
|
||||
dom.innerHTML = translationIconSvg;
|
||||
|
||||
// 点击事件
|
||||
dom.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// 初始化翻译气泡状态字段
|
||||
this.translationTooltipField = StateField.define<readonly Tooltip[]>({
|
||||
create: () => [],
|
||||
update: (tooltips, tr) => {
|
||||
// 检查是否有特定的状态效果来更新tooltips
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(this.setTranslationTooltip)) {
|
||||
return effect.value ? [effect.value] : [];
|
||||
}
|
||||
}
|
||||
|
||||
// 显示翻译气泡
|
||||
showTranslationTooltip(view);
|
||||
});
|
||||
// 如果文档或选择变化,隐藏气泡
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
|
||||
return { dom };
|
||||
}
|
||||
}];
|
||||
}
|
||||
// 初始化翻译按钮状态字段
|
||||
this.translationButtonField = StateField.define<readonly Tooltip[]>({
|
||||
create: (state) => this.getTranslationButtonTooltips(state),
|
||||
update: (tooltips, tr) => {
|
||||
// 如果文档或选择变化,重新计算tooltip
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return this.getTranslationButtonTooltips(tr.state);
|
||||
}
|
||||
|
||||
// 检查是否有翻译气泡显示,如果有则不显示按钮
|
||||
if (tr.state.field(this.translationTooltipField).length > 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示翻译气泡
|
||||
*/
|
||||
function showTranslationTooltip(view: EditorView) {
|
||||
if (!selectedTextForTranslation) return;
|
||||
|
||||
// 创建翻译气泡
|
||||
const tooltip = createTranslationTooltip(view, selectedTextForTranslation);
|
||||
|
||||
// 更新状态以显示气泡
|
||||
view.dispatch({
|
||||
effects: setTranslationTooltip.of(tooltip)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译按钮的状态字段
|
||||
*/
|
||||
const translationButtonField = StateField.define<readonly Tooltip[]>({
|
||||
create(state) {
|
||||
return getTranslationButtonTooltips(state);
|
||||
},
|
||||
|
||||
update(tooltips, tr) {
|
||||
// 如果文档或选择变化,重新计算tooltip
|
||||
if (tr.docChanged || tr.selection) {
|
||||
return getTranslationButtonTooltips(tr.state);
|
||||
}
|
||||
/**
|
||||
* 根据当前选择获取翻译按钮tooltip
|
||||
*/
|
||||
private getTranslationButtonTooltips(state: any): readonly Tooltip[] {
|
||||
// 如果气泡已显示,则不显示按钮
|
||||
if (state.field(this.translationTooltipField).length > 0) return [];
|
||||
|
||||
// 检查是否有翻译气泡显示,如果有则不显示按钮
|
||||
if (tr.state.field(translationTooltipField).length > 0) {
|
||||
const selection = state.selection.main;
|
||||
|
||||
// 如果没有选中文本,不显示按钮
|
||||
if (selection.empty) return [];
|
||||
|
||||
// 获取选中的文本
|
||||
const selectedText = state.sliceDoc(selection.from, selection.to);
|
||||
|
||||
// 检查文本是否只包含空格
|
||||
if (!selectedText.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return tooltips;
|
||||
},
|
||||
// 检查文本长度条件
|
||||
if (selectedText.length < this.config.minSelectionLength ||
|
||||
selectedText.length > this.config.maxTranslationLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 返回翻译按钮tooltip配置
|
||||
return [{
|
||||
pos: selection.to,
|
||||
above: false,
|
||||
strictSide: true,
|
||||
arrow: false,
|
||||
create: (view) => {
|
||||
// 创建按钮DOM
|
||||
const dom = document.createElement('div');
|
||||
dom.className = 'cm-translator-button';
|
||||
dom.innerHTML = TRANSLATION_ICON_SVG;
|
||||
|
||||
provide: field => showTooltip.computeN([field], state => state.field(field))
|
||||
});
|
||||
// 点击事件
|
||||
dom.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 显示翻译气泡
|
||||
this.showTranslationTooltip(view);
|
||||
});
|
||||
|
||||
return { dom };
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示翻译气泡
|
||||
*/
|
||||
private showTranslationTooltip(view: EditorView) {
|
||||
// 直接从当前选择获取文本
|
||||
const selection = view.state.selection.main;
|
||||
if (selection.empty) return;
|
||||
|
||||
const selectedText = view.state.sliceDoc(selection.from, selection.to);
|
||||
if (!selectedText.trim()) return;
|
||||
|
||||
// 创建翻译气泡
|
||||
const tooltip = createTranslationTooltip(view, selectedText);
|
||||
|
||||
// 更新状态以显示气泡
|
||||
view.dispatch({
|
||||
effects: this.setTranslationTooltip.of(tooltip)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建扩展
|
||||
*/
|
||||
createExtension(): Extension {
|
||||
return [
|
||||
// 翻译按钮tooltip
|
||||
this.translationButtonField,
|
||||
// 翻译气泡tooltip
|
||||
this.translationTooltipField,
|
||||
|
||||
// 添加基础样式
|
||||
EditorView.baseTheme({
|
||||
".cm-translator-button": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
background: "var(--bg-secondary, transparent)",
|
||||
color: "var(--text-muted, #4285f4)",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
borderRadius: "3px",
|
||||
padding: "2px",
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
||||
userSelect: "none",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||
}
|
||||
},
|
||||
|
||||
// 翻译气泡样式
|
||||
".cm-translation-tooltip": {
|
||||
background: "var(--bg-secondary, #fff)",
|
||||
color: "var(--text-primary, #333)",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
borderRadius: "3px",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
padding: "8px",
|
||||
maxWidth: "300px",
|
||||
maxHeight: "200px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
fontFamily: "var(--font-family, system-ui, -apple-system, sans-serif)",
|
||||
fontSize: "11px",
|
||||
userSelect: "none",
|
||||
cursor: "grab"
|
||||
},
|
||||
|
||||
// 拖拽状态样式
|
||||
".cm-translation-dragging": {
|
||||
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)",
|
||||
zIndex: "1000",
|
||||
cursor: "grabbing !important"
|
||||
},
|
||||
|
||||
".cm-translation-header": {
|
||||
marginBottom: "8px",
|
||||
flexShrink: "0"
|
||||
},
|
||||
|
||||
".cm-translation-controls": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "nowrap"
|
||||
},
|
||||
|
||||
".cm-translation-select": {
|
||||
padding: "2px 4px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, #f5f5f5)",
|
||||
fontSize: "11px",
|
||||
color: "var(--text-primary, #333)",
|
||||
flex: "1",
|
||||
minWidth: "0",
|
||||
maxWidth: "80px"
|
||||
},
|
||||
|
||||
".cm-translation-swap": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, transparent)",
|
||||
color: "var(--text-muted, #666)",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
flexShrink: "0",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||
}
|
||||
},
|
||||
|
||||
// 滚动容器
|
||||
".cm-translation-scroll-container": {
|
||||
overflowY: "auto",
|
||||
flex: "1",
|
||||
minHeight: "0"
|
||||
},
|
||||
|
||||
".cm-translation-result": {
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
},
|
||||
|
||||
".cm-translation-result-header": {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: "4px"
|
||||
},
|
||||
|
||||
".cm-translation-result-wrapper": {
|
||||
position: "relative",
|
||||
width: "100%"
|
||||
},
|
||||
|
||||
".cm-translation-copy-btn": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, transparent)",
|
||||
color: "var(--text-muted, #666)",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
position: "absolute",
|
||||
top: "4px",
|
||||
right: "4px",
|
||||
zIndex: "2",
|
||||
opacity: "0.7",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))",
|
||||
opacity: "1"
|
||||
},
|
||||
"&.copied": {
|
||||
background: "var(--bg-success, #4caf50)",
|
||||
color: "white",
|
||||
border: "1px solid var(--bg-success, #4caf50)",
|
||||
opacity: "1"
|
||||
}
|
||||
},
|
||||
|
||||
".cm-translation-target": {
|
||||
padding: "6px",
|
||||
paddingRight: "28px", // 为复制按钮留出空间
|
||||
background: "var(--bg-primary, rgba(66, 133, 244, 0.05))",
|
||||
color: "var(--text-primary, #333)",
|
||||
borderRadius: "3px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word"
|
||||
},
|
||||
|
||||
".cm-translation-notice": {
|
||||
fontSize: "10px",
|
||||
color: "var(--text-muted, #888)",
|
||||
padding: "2px 0",
|
||||
fontStyle: "italic",
|
||||
textAlign: "center",
|
||||
marginBottom: "2px"
|
||||
},
|
||||
|
||||
".cm-translation-error": {
|
||||
color: "var(--text-danger, #d32f2f)",
|
||||
fontStyle: "italic"
|
||||
},
|
||||
|
||||
".cm-translation-loading": {
|
||||
padding: "8px",
|
||||
textAlign: "center",
|
||||
color: "var(--text-muted, #666)",
|
||||
fontSize: "11px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "6px"
|
||||
},
|
||||
|
||||
".cm-translation-loading::before": {
|
||||
content: "''",
|
||||
display: "inline-block",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
border: "2px solid var(--text-muted, #666)",
|
||||
borderTopColor: "transparent",
|
||||
animation: "cm-translation-spin 1s linear infinite"
|
||||
},
|
||||
|
||||
"@keyframes cm-translation-spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" }
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建翻译扩展
|
||||
*/
|
||||
export function createTranslatorExtension(config?: Partial<TranslatorConfig>): Extension {
|
||||
// 更新配置
|
||||
currentConfig = { ...defaultConfig, ...config };
|
||||
|
||||
return [
|
||||
// 翻译按钮tooltip
|
||||
translationButtonField,
|
||||
// 翻译气泡tooltip
|
||||
translationTooltipField,
|
||||
|
||||
// 添加基础样式
|
||||
EditorView.baseTheme({
|
||||
".cm-translator-button": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
cursor: "pointer",
|
||||
background: "var(--bg-secondary, transparent)",
|
||||
color: "var(--text-muted, #4285f4)",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
borderRadius: "3px",
|
||||
padding: "2px",
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.08)",
|
||||
userSelect: "none",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||
}
|
||||
},
|
||||
|
||||
// 翻译气泡样式
|
||||
".cm-translation-tooltip": {
|
||||
background: "var(--bg-secondary, #fff)",
|
||||
color: "var(--text-primary, #333)",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
borderRadius: "3px",
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
padding: "8px",
|
||||
maxWidth: "300px",
|
||||
maxHeight: "200px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
fontFamily: "var(--font-family, system-ui, -apple-system, sans-serif)",
|
||||
fontSize: "11px"
|
||||
},
|
||||
|
||||
".cm-translation-header": {
|
||||
marginBottom: "8px",
|
||||
flexShrink: "0"
|
||||
},
|
||||
|
||||
".cm-translation-controls": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "nowrap"
|
||||
},
|
||||
|
||||
".cm-translation-select": {
|
||||
padding: "2px 4px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, #f5f5f5)",
|
||||
fontSize: "11px",
|
||||
color: "var(--text-primary, #333)",
|
||||
flex: "1",
|
||||
minWidth: "0",
|
||||
maxWidth: "80px"
|
||||
},
|
||||
|
||||
".cm-translation-swap": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, transparent)",
|
||||
color: "var(--text-muted, #666)",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
flexShrink: "0",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))"
|
||||
}
|
||||
},
|
||||
|
||||
// 滚动容器
|
||||
".cm-translation-scroll-container": {
|
||||
overflowY: "auto",
|
||||
flex: "1",
|
||||
minHeight: "0"
|
||||
},
|
||||
|
||||
".cm-translation-result": {
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
},
|
||||
|
||||
".cm-translation-result-header": {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: "4px"
|
||||
},
|
||||
|
||||
".cm-translation-result-wrapper": {
|
||||
position: "relative",
|
||||
width: "100%"
|
||||
},
|
||||
|
||||
".cm-translation-copy-btn": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid var(--border-color, #dadce0)",
|
||||
background: "var(--bg-primary, transparent)",
|
||||
color: "var(--text-muted, #666)",
|
||||
cursor: "pointer",
|
||||
padding: "0",
|
||||
position: "absolute",
|
||||
top: "4px",
|
||||
right: "4px",
|
||||
zIndex: "2",
|
||||
opacity: "0.7",
|
||||
"&:hover": {
|
||||
background: "var(--bg-hover, rgba(66, 133, 244, 0.1))",
|
||||
opacity: "1"
|
||||
},
|
||||
"&.copied": {
|
||||
background: "var(--bg-success, #4caf50)",
|
||||
color: "white",
|
||||
border: "1px solid var(--bg-success, #4caf50)",
|
||||
opacity: "1"
|
||||
}
|
||||
},
|
||||
|
||||
".cm-translation-target": {
|
||||
padding: "6px",
|
||||
paddingRight: "28px", // 为复制按钮留出空间
|
||||
background: "var(--bg-primary, rgba(66, 133, 244, 0.05))",
|
||||
color: "var(--text-primary, #333)",
|
||||
borderRadius: "3px",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word"
|
||||
},
|
||||
|
||||
".cm-translation-notice": {
|
||||
fontSize: "10px",
|
||||
color: "var(--text-muted, #888)",
|
||||
padding: "2px 0",
|
||||
fontStyle: "italic",
|
||||
textAlign: "center",
|
||||
marginBottom: "2px"
|
||||
},
|
||||
|
||||
".cm-translation-error": {
|
||||
color: "var(--text-danger, #d32f2f)",
|
||||
fontStyle: "italic"
|
||||
},
|
||||
|
||||
".cm-translation-loading": {
|
||||
padding: "8px",
|
||||
textAlign: "center",
|
||||
color: "var(--text-muted, #666)",
|
||||
fontSize: "11px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "6px"
|
||||
},
|
||||
|
||||
".cm-translation-loading::before": {
|
||||
content: "''",
|
||||
display: "inline-block",
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
border: "2px solid var(--text-muted, #666)",
|
||||
borderTopColor: "transparent",
|
||||
animation: "cm-translation-spin 1s linear infinite"
|
||||
},
|
||||
|
||||
"@keyframes cm-translation-spin": {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" }
|
||||
}
|
||||
})
|
||||
];
|
||||
const translatorExtension = new TranslatorExtension(config);
|
||||
return translatorExtension.createExtension();
|
||||
}
|
||||
|
||||
export default createTranslatorExtension;
|
||||
export default createTranslatorExtension;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -158,14 +158,12 @@ export const checkboxFactory: ExtensionFactory = {
|
||||
export const translatorFactory: ExtensionFactory = {
|
||||
create(config: any) {
|
||||
return createTranslatorExtension({
|
||||
defaultTranslator: config.defaultTranslator || 'bing',
|
||||
minSelectionLength: config.minSelectionLength || 2,
|
||||
maxTranslationLength: config.maxTranslationLength || 5000,
|
||||
});
|
||||
},
|
||||
getDefaultConfig() {
|
||||
return {
|
||||
defaultTranslator: 'bing',
|
||||
minSelectionLength: 2,
|
||||
maxTranslationLength: 5000,
|
||||
};
|
||||
|
||||
@@ -8,12 +8,39 @@ import SettingItem from '../components/SettingItem.vue';
|
||||
import { SystemThemeType, LanguageType } from '@/../bindings/voidraft/internal/models/models';
|
||||
import { defaultDarkColors } from '@/views/editor/theme/dark';
|
||||
import { defaultLightColors } from '@/views/editor/theme/light';
|
||||
import { createDebounce } from '@/common/utils/debounce';
|
||||
import { createTimerManager } from '@/common/utils/timerUtils';
|
||||
import PickColors from 'vue-pick-colors';
|
||||
|
||||
const { t } = useI18n();
|
||||
const configStore = useConfigStore();
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
// 创建防抖函数实例
|
||||
const { debouncedFn: debouncedUpdateColor } = createDebounce(
|
||||
(colorKey: string, value: string) => updateLocalColor(colorKey, value),
|
||||
{ delay: 100 }
|
||||
);
|
||||
|
||||
const { debouncedFn: debouncedResetTheme } = createDebounce(
|
||||
async () => {
|
||||
const themeType = activeThemeType.value;
|
||||
const success = await themeStore.resetThemeColors(themeType);
|
||||
|
||||
if (success) {
|
||||
tempColors.value = {
|
||||
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||
};
|
||||
hasUnsavedChanges.value = false;
|
||||
}
|
||||
},
|
||||
{ delay: 300 }
|
||||
);
|
||||
|
||||
// 创建定时器管理器
|
||||
const resetTimer = createTimerManager();
|
||||
|
||||
// 添加临时颜色状态
|
||||
const tempColors = ref({
|
||||
darkTheme: { ...defaultDarkColors },
|
||||
@@ -25,36 +52,19 @@ const hasUnsavedChanges = ref(false);
|
||||
|
||||
// 重置按钮状态
|
||||
const resetButtonState = ref({
|
||||
confirming: false,
|
||||
timer: null as number | null
|
||||
confirming: false
|
||||
});
|
||||
|
||||
// 防抖函数
|
||||
const debounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: number | undefined;
|
||||
|
||||
return function(...args: Parameters<T>): void {
|
||||
clearTimeout(timeout);
|
||||
timeout = window.setTimeout(() => {
|
||||
func(...args);
|
||||
}, wait);
|
||||
};
|
||||
};
|
||||
// 当前激活的主题类型
|
||||
const isDarkMode = computed(() =>
|
||||
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
|
||||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
);
|
||||
|
||||
// 当前激活的主题类型(基于当前系统主题)
|
||||
const activeThemeType = computed(() => {
|
||||
const isDark =
|
||||
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
|
||||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
return isDark ? 'darkTheme' : 'lightTheme';
|
||||
});
|
||||
const activeThemeType = computed(() => isDarkMode.value ? 'darkTheme' : 'lightTheme');
|
||||
|
||||
// 当前主题的颜色配置 - 使用临时状态
|
||||
// 当前主题的颜色配置
|
||||
const currentColors = computed(() => {
|
||||
const themeType = activeThemeType.value;
|
||||
return tempColors.value[themeType] ||
|
||||
@@ -62,144 +72,85 @@ const currentColors = computed(() => {
|
||||
});
|
||||
|
||||
// 获取当前主题模式
|
||||
const currentThemeMode = computed(() => {
|
||||
const isDark =
|
||||
themeStore.currentTheme === SystemThemeType.SystemThemeDark ||
|
||||
(themeStore.currentTheme === SystemThemeType.SystemThemeAuto &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
|
||||
return isDark ? 'dark' : 'light';
|
||||
});
|
||||
const currentThemeMode = computed(() => isDarkMode.value ? 'dark' : 'light');
|
||||
|
||||
// 监听主题颜色变更,更新临时颜色
|
||||
// 监听主题颜色变更,
|
||||
watch(
|
||||
() => themeStore.themeColors,
|
||||
(newValue) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
tempColors.value = {
|
||||
darkTheme: { ...newValue.darkTheme },
|
||||
lightTheme: { ...newValue.lightTheme }
|
||||
};
|
||||
tempColors.value.darkTheme = { ...newValue.darkTheme };
|
||||
tempColors.value.lightTheme = { ...newValue.lightTheme };
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
// 初始化时加载主题颜色
|
||||
onMounted(() => {
|
||||
// 使用themeStore中的颜色作为初始值
|
||||
tempColors.value = {
|
||||
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||
};
|
||||
});
|
||||
|
||||
// 颜色配置分组
|
||||
const colorGroups = computed(() => [
|
||||
// 颜色配置
|
||||
const colorConfig = [
|
||||
{
|
||||
key: 'basic',
|
||||
title: t('settings.themeColors.basic'),
|
||||
colors: [
|
||||
{ key: 'background', label: t('settings.themeColors.background') },
|
||||
{ key: 'backgroundSecondary', label: t('settings.themeColors.backgroundSecondary') },
|
||||
{ key: 'surface', label: t('settings.themeColors.surface') }
|
||||
]
|
||||
colors: ['background', 'backgroundSecondary', 'surface']
|
||||
},
|
||||
{
|
||||
key: 'text',
|
||||
title: t('settings.themeColors.text'),
|
||||
colors: [
|
||||
{ key: 'foreground', label: t('settings.themeColors.foreground') },
|
||||
{ key: 'foregroundSecondary', label: t('settings.themeColors.foregroundSecondary') },
|
||||
{ key: 'comment', label: t('settings.themeColors.comment') }
|
||||
]
|
||||
key: 'text',
|
||||
colors: ['foreground', 'foregroundSecondary', 'comment']
|
||||
},
|
||||
{
|
||||
key: 'syntax',
|
||||
title: t('settings.themeColors.syntax'),
|
||||
colors: [
|
||||
{ key: 'keyword', label: t('settings.themeColors.keyword') },
|
||||
{ key: 'string', label: t('settings.themeColors.string') },
|
||||
{ key: 'function', label: t('settings.themeColors.function') },
|
||||
{ key: 'number', label: t('settings.themeColors.number') },
|
||||
{ key: 'operator', label: t('settings.themeColors.operator') },
|
||||
{ key: 'variable', label: t('settings.themeColors.variable') },
|
||||
{ key: 'type', label: t('settings.themeColors.type') }
|
||||
]
|
||||
colors: ['keyword', 'string', 'function', 'number', 'operator', 'variable', 'type']
|
||||
},
|
||||
{
|
||||
key: 'interface',
|
||||
title: t('settings.themeColors.interface'),
|
||||
colors: [
|
||||
{ key: 'cursor', label: t('settings.themeColors.cursor') },
|
||||
{ key: 'selection', label: t('settings.themeColors.selection') },
|
||||
{ key: 'selectionBlur', label: t('settings.themeColors.selectionBlur') },
|
||||
{ key: 'activeLine', label: t('settings.themeColors.activeLine') },
|
||||
{ key: 'lineNumber', label: t('settings.themeColors.lineNumber') },
|
||||
{ key: 'activeLineNumber', label: t('settings.themeColors.activeLineNumber') }
|
||||
]
|
||||
colors: ['cursor', 'selection', 'selectionBlur', 'activeLine', 'lineNumber', 'activeLineNumber']
|
||||
},
|
||||
{
|
||||
key: 'border',
|
||||
title: t('settings.themeColors.border'),
|
||||
colors: [
|
||||
{ key: 'borderColor', label: t('settings.themeColors.borderColor') },
|
||||
{ key: 'borderLight', label: t('settings.themeColors.borderLight') }
|
||||
]
|
||||
colors: ['borderColor', 'borderLight']
|
||||
},
|
||||
{
|
||||
key: 'search',
|
||||
title: t('settings.themeColors.search'),
|
||||
colors: [
|
||||
{ key: 'searchMatch', label: t('settings.themeColors.searchMatch') },
|
||||
{ key: 'matchingBracket', label: t('settings.themeColors.matchingBracket') }
|
||||
]
|
||||
colors: ['searchMatch', 'matchingBracket']
|
||||
}
|
||||
]);
|
||||
];
|
||||
|
||||
// 颜色配置分组
|
||||
const colorGroups = computed(() =>
|
||||
colorConfig.map(group => ({
|
||||
key: group.key,
|
||||
title: t(`settings.themeColors.${group.key}`),
|
||||
colors: group.colors.map(colorKey => ({
|
||||
key: colorKey,
|
||||
label: t(`settings.themeColors.${colorKey}`)
|
||||
}))
|
||||
}))
|
||||
);
|
||||
|
||||
// 处理重置按钮点击
|
||||
const handleResetClick = () => {
|
||||
if (resetButtonState.value.confirming) {
|
||||
// 如果已经在确认状态,执行重置操作
|
||||
resetCurrentTheme();
|
||||
|
||||
// 重置按钮状态
|
||||
|
||||
debouncedResetTheme();
|
||||
|
||||
resetButtonState.value.confirming = false;
|
||||
if (resetButtonState.value.timer !== null) {
|
||||
clearTimeout(resetButtonState.value.timer);
|
||||
resetButtonState.value.timer = null;
|
||||
}
|
||||
resetTimer.clear();
|
||||
} else {
|
||||
// 进入确认状态
|
||||
resetButtonState.value.confirming = true;
|
||||
|
||||
// 设置3秒后自动恢复
|
||||
resetButtonState.value.timer = window.setTimeout(() => {
|
||||
resetTimer.set(() => {
|
||||
resetButtonState.value.confirming = false;
|
||||
resetButtonState.value.timer = null;
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置当前主题为默认配置
|
||||
const resetCurrentTheme = debounce(async () => {
|
||||
// 使用themeStore的原子重置操作
|
||||
const themeType = activeThemeType.value;
|
||||
const success = await themeStore.resetThemeColors(themeType);
|
||||
|
||||
if (success) {
|
||||
// 更新临时颜色状态
|
||||
tempColors.value = {
|
||||
darkTheme: { ...themeStore.themeColors.darkTheme },
|
||||
lightTheme: { ...themeStore.themeColors.lightTheme }
|
||||
};
|
||||
|
||||
// 标记没有未保存的更改
|
||||
hasUnsavedChanges.value = false;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// 更新本地颜色配置 - 仅更新临时状态,不提交到后端
|
||||
// 更新本地颜色配置
|
||||
const updateLocalColor = (colorKey: string, value: string) => {
|
||||
const themeType = activeThemeType.value;
|
||||
|
||||
@@ -211,14 +162,10 @@ const updateLocalColor = (colorKey: string, value: string) => {
|
||||
[colorKey]: value
|
||||
}
|
||||
};
|
||||
|
||||
// 标记有未保存的更改
|
||||
|
||||
hasUnsavedChanges.value = true;
|
||||
};
|
||||
|
||||
// 防抖包装的颜色更新函数
|
||||
const updateColor = debounce(updateLocalColor, 100);
|
||||
|
||||
// 应用颜色更改到系统
|
||||
const applyChanges = async () => {
|
||||
try {
|
||||
@@ -290,7 +237,7 @@ const showPickerMap = ref<Record<string, boolean>>({});
|
||||
|
||||
// 颜色变更处理
|
||||
const handleColorChange = (colorKey: string, value: string) => {
|
||||
updateColor(colorKey, value);
|
||||
debouncedUpdateColor(colorKey, value);
|
||||
};
|
||||
|
||||
// 颜色选择器关闭处理
|
||||
@@ -374,7 +321,7 @@ const handlePickerClose = () => {
|
||||
<input
|
||||
type="text"
|
||||
:value="currentColors[color.key] || ''"
|
||||
@input="updateColor(color.key, ($event.target as HTMLInputElement).value)"
|
||||
@input="debouncedUpdateColor(color.key, ($event.target as HTMLInputElement).value)"
|
||||
class="color-text-input"
|
||||
:placeholder="t('settings.colorValue')"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user