import { defineStore } from 'pinia'; import apiClient from '../utils/apiClient'; // 使用统一的 apiClient import { ref, computed, watch, nextTick } from 'vue'; // 导入 nextTick import type { ITheme } from 'xterm'; import type { TerminalTheme } from '../types/terminal-theme.types'; // 引用本地类型 import type { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types'; // 引用本地类型 import { defaultXtermTheme, defaultUiTheme } from '../features/appearance/config/default-themes'; // 保持 .ts import { presetTerminalThemes } from '../features/appearance/config/iterm-themes'; // <-- 导入预设主题 // Helper function to safely parse JSON export const safeJsonParse = (jsonString: string | undefined | null, defaultValue: T): T => { // Add export if (!jsonString) return defaultValue; try { return JSON.parse(jsonString); } catch (e) { console.error("JSON 解析失败:", e); return defaultValue; } }; export const useAppearanceStore = defineStore('appearance', () => { // --- State --- const isLoading = ref(false); const error = ref(null); const isStyleCustomizerVisible = ref(false); // 控制样式编辑器可见性 // Appearance Settings State const appearanceSettings = ref>({}); // 从 API 获取的原始设置 const allTerminalThemes = ref([]); // 重命名: 存储从后端获取的所有主题 // --- Computed Properties (Getters) --- // 移除 availableTerminalThemes 计算属性,直接使用 allTerminalThemes // 当前应用的 UI 主题 (CSS 变量对象) const currentUiTheme = computed>(() => { return safeJsonParse(appearanceSettings.value.customUiTheme, defaultUiTheme); }); // 当前激活的终端主题 ID const activeTerminalThemeId = computed(() => appearanceSettings.value.activeTerminalThemeId); // 当前应用的终端主题对象 (ITheme) const currentTerminalTheme = computed(() => { const activeId = activeTerminalThemeId.value; // number | null | undefined if (activeId === null || activeId === undefined || allTerminalThemes.value.length === 0) { // 如果没有激活 ID 或列表为空,查找默认主题 // TODO: 需要确认默认主题的识别方式 (preset_key='default' 或 name='默认') const defaultTheme = allTerminalThemes.value.find(t => t.name === '默认'); // 假设按名称查找 return defaultTheme ? defaultTheme.themeData : defaultXtermTheme; } // 根据数字 ID 查找 (需要将 theme._id 转回数字比较) const activeTheme = allTerminalThemes.value.find(t => parseInt(t._id ?? '-1', 10) === activeId); return activeTheme ? activeTheme.themeData : defaultXtermTheme; // 找不到也回退到 xterm 默认 }); // 当前终端字体设置 const currentTerminalFontFamily = computed(() => { return appearanceSettings.value.terminalFontFamily || 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"'; // 提供默认值 }); // 当前终端字体大小 const currentTerminalFontSize = computed(() => { // 提供默认值 14,如果后端没有设置或设置无效 const size = appearanceSettings.value.terminalFontSize; return typeof size === 'number' && size > 0 ? size : 14; }); // 页面背景图片 URL const pageBackgroundImage = computed(() => appearanceSettings.value.pageBackgroundImage); // 终端背景图片 URL const terminalBackgroundImage = computed(() => appearanceSettings.value.terminalBackgroundImage); // 当前编辑器字体大小 const currentEditorFontSize = computed(() => { // 提供默认值 14,如果后端没有设置或设置无效 const size = appearanceSettings.value.editorFontSize; return typeof size === 'number' && size > 0 ? size : 14; }); // 终端背景是否启用 const isTerminalBackgroundEnabled = computed(() => { // 提供默认值 true,如果后端没有设置或设置无效 const enabled = appearanceSettings.value.terminalBackgroundEnabled; return typeof enabled === 'boolean' ? enabled : true; // 默认启用 }); // --- Actions --- /** * 加载所有外观相关设置 (外观设置 + 终端主题列表) */ async function loadInitialAppearanceData() { isLoading.value = true; error.value = null; try { // 并行加载外观设置和主题列表 const [settingsResponse, themesResponse] = await Promise.all([ apiClient.get('/appearance'), // 使用 apiClient apiClient.get('/terminal-themes') // 使用 apiClient ]); appearanceSettings.value = settingsResponse.data; allTerminalThemes.value = themesResponse.data; // 更新 allTerminalThemes // 应用加载的 UI 主题 applyUiTheme(currentUiTheme.value); // 应用背景 applyPageBackground(); // 终端主题将由 Terminal 组件根据 activeTerminalThemeId 自动应用 } catch (err: any) { console.error('加载外观数据失败:', err); error.value = err.response?.data?.message || err.message || '加载外观数据失败'; // 出错时应用默认值 appearanceSettings.value = {}; // 清空可能不完整的设置 allTerminalThemes.value = []; // 清空 allTerminalThemes applyUiTheme(defaultUiTheme); applyPageBackground(); // 应用默认背景(可能为空) } finally { isLoading.value = false; } } /** * 切换样式编辑器面板的可见性。 * @param visible 可选,强制设置可见性 */ function toggleStyleCustomizer(visible?: boolean) { isStyleCustomizerVisible.value = visible === undefined ? !isStyleCustomizerVisible.value : visible; console.log('[AppearanceStore] Style Customizer visibility toggled:', isStyleCustomizerVisible.value); } /** * 更新外观设置 (不包括主题列表管理) * @param updates 要更新的设置项 (activeTerminalThemeId 应为 number | null) */ async function updateAppearanceSettings(updates: UpdateAppearanceDto) { try { // 移除预设主题闪烁修复逻辑,不再需要 const response = await apiClient.put('/appearance', updates); // 使用 apiClient // 使用后端返回的最新设置更新本地状态 appearanceSettings.value = response.data; console.log('[AppearanceStore] 外观设置已更新:', appearanceSettings.value); // 如果 UI 主题或背景更新,重新应用 if (updates.customUiTheme !== undefined) applyUiTheme(currentUiTheme.value); if (updates.pageBackgroundImage !== undefined) applyPageBackground(); // 移除 pageBackgroundOpacity 检查 // 终端相关设置由 Terminal 组件监听应用 // 注意:terminalBackgroundEnabled 的应用逻辑在 Terminal 组件中处理 } catch (err: any) { console.error('更新外观设置失败:', err); throw new Error(err.response?.data?.message || err.message || '更新外观设置失败'); } } /** * 保存当前编辑器中的自定义 UI 主题到后端。 * @param uiTheme UI 主题对象 */ async function saveCustomUiTheme(uiTheme: Record) { await updateAppearanceSettings({ customUiTheme: JSON.stringify(uiTheme) }); } /** * 重置为默认 UI 主题并保存。 */ async function resetCustomUiTheme() { await saveCustomUiTheme(defaultUiTheme); } /** * 设置激活的终端主题 * @param themeId 主题的字符串 ID (来自 UI) 或 null (用于重置,但新逻辑下不直接使用 null) */ async function setActiveTerminalTheme(themeId: string) { // 参数改为 string,不允许 null const previousActiveId = appearanceSettings.value.activeTerminalThemeId; // 记录之前的数字 ID 或 null // 1. 将传入的字符串 ID 转换为数字 const idNum = parseInt(themeId, 10); if (isNaN(idNum)) { console.error(`[AppearanceStore] setActiveTerminalTheme 接收到无效的数字 ID 字符串: ${themeId}`); throw new Error(`无效的主题 ID: ${themeId}`); } // 2. 立即更新前端本地状态 (使用数字 ID) appearanceSettings.value.activeTerminalThemeId = idNum; console.log(`[AppearanceStore] Applied theme locally (ID): ${idNum}`); // 3. 更新后端 (发送数字 ID) try { await updateAppearanceSettings({ activeTerminalThemeId: idNum }); console.log(`[AppearanceStore] Notified backend. Sent activeTerminalThemeId: ${idNum}`); } catch (error) { // 如果更新后端失败,回滚前端状态 console.error('[AppearanceStore] Failed to update backend activeTerminalThemeId:', error); appearanceSettings.value.activeTerminalThemeId = previousActiveId; // 回滚到之前的数字 ID 或 null throw new Error(`应用主题失败: ${error instanceof Error ? error.message : String(error)}`); } } /** * 设置终端字体 * @param fontFamily 字体列表字符串 */ async function setTerminalFontFamily(fontFamily: string) { await updateAppearanceSettings({ terminalFontFamily: fontFamily }); } /** * 设置终端字体大小 * @param size 字体大小 (数字) */ async function setTerminalFontSize(size: number) { await updateAppearanceSettings({ terminalFontSize: size }); } /** * 设置编辑器字体大小 * @param size 字体大小 (数字) */ async function setEditorFontSize(size: number) { await updateAppearanceSettings({ editorFontSize: size }); } /** * 设置终端背景是否启用 * @param enabled 是否启用 */ async function setTerminalBackgroundEnabled(enabled: boolean) { console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 调用,准备发送给后端的值: ${enabled}`); // 添加日志 await updateAppearanceSettings({ terminalBackgroundEnabled: enabled }); console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 更新后端调用完成。`); // 添加日志 } // --- 终端主题列表管理 Actions --- /** // 移除 reloadTerminalThemes,统一由 loadInitialAppearanceData 处理加载 /** * 创建新的终端主题 * @param name 主题名称 * @param themeData 主题数据 (ITheme) */ async function createTerminalTheme(name: string, themeData: ITheme) { try { await apiClient.post('/terminal-themes', { name, themeData }); // 使用 apiClient await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('创建终端主题失败:', err); throw new Error(err.response?.data?.message || err.message || '创建终端主题失败'); } } /** * 更新终端主题 * @param id 主题 ID * @param name 新名称 * @param themeData 新主题数据 */ async function updateTerminalTheme(id: string, name: string, themeData: ITheme) { try { await apiClient.put(`/terminal-themes/${id}`, { name, themeData }); // 使用 apiClient await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('更新终端主题失败:', err); throw new Error(err.response?.data?.message || err.message || '更新终端主题失败'); } } /** * 删除终端主题 * @param id 主题 ID */ async function deleteTerminalTheme(id: string) { try { await apiClient.delete(`/terminal-themes/${id}`); // 使用 apiClient // 如果删除的是当前激活的主题,则切换回默认主题 ID // 需要将字符串 id 转换为数字进行比较 const idNum = parseInt(id, 10); if (!isNaN(idNum) && activeTerminalThemeId.value === idNum) { // 查找默认主题的数字 ID (这里假设默认主题 ID 为 1,实际应从配置或查询获取) // TODO: 需要一种可靠的方式获取默认主题的数字 ID const defaultThemeIdNum = 1; // 临时硬编码,需要改进 console.log(`[AppearanceStore] 删除的主题是当前激活主题,尝试切换到默认主题 ID: ${defaultThemeIdNum}`); await setActiveTerminalTheme(defaultThemeIdNum.toString()); // setActiveTerminalTheme 需要字符串 ID } await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('删除终端主题失败:', err); throw new Error(err.response?.data?.message || err.message || '删除终端主题失败'); } } /** * 导入终端主题文件 * @param file File 对象 * @param name 可选,如果提供则覆盖文件名作为主题名 */ async function importTerminalTheme(file: File, name?: string) { const formData = new FormData(); formData.append('themeFile', file); if (name) { formData.append('name', name); } try { await apiClient.post('/terminal-themes/import', formData, { // 使用 apiClient headers: { 'Content-Type': 'multipart/form-data' } }); await loadInitialAppearanceData(); // 重新加载所有数据以更新列表 } catch (err: any) { console.error('导入终端主题失败:', err); throw new Error(err.response?.data?.message || err.message || '导入终端主题失败'); } } /** * 导出终端主题文件 * @param id 主题 ID */ async function exportTerminalTheme(id: string) { try { const response = await apiClient.get(`/terminal-themes/${id}/export`, { // 使用 apiClient responseType: 'blob' // 重要:接收二进制数据 }); // 从响应头获取文件名 const contentDisposition = response.headers['content-disposition']; let filename = `terminal_theme_${id}.json`; // 默认文件名 if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i); if (filenameMatch && filenameMatch.length > 1) { filename = filenameMatch[1]; } } // 创建下载链接并触发下载 const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = url; link.setAttribute('download', filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } catch (err: any) { console.error('导出终端主题失败:', err); throw new Error(err.response?.data?.message || err.message || '导出终端主题失败'); } } /** * 按需加载单个终端主题的详细数据 * @param themeId 主题的字符串 ID * @returns 返回主题的 ITheme 数据,如果找不到或加载失败则返回 null */ async function loadTerminalThemeData(themeId: string): Promise { // 1. 尝试从已加载的列表中查找 const existingTheme = allTerminalThemes.value.find(t => t._id === themeId); // 2. 如果找到且已有 themeData,直接返回 if (existingTheme?.themeData && Object.keys(existingTheme.themeData).length > 0) { console.log(`[AppearanceStore] Theme data for ${themeId} already loaded.`); return existingTheme.themeData; } // 3. 如果未找到或缺少 themeData,从后端加载 console.log(`[AppearanceStore] Loading theme data for ${themeId} from backend...`); try { const response = await apiClient.get(`/terminal-themes/${themeId}`); // 假设后端提供此接口 const fullTheme = response.data; if (fullTheme && fullTheme.themeData) { // 更新 allTerminalThemes 列表中的对应项 const index = allTerminalThemes.value.findIndex(t => t._id === themeId); if (index !== -1) { // 确保响应性,可以考虑替换整个对象或使用 Vue.set (在 Vue 3 中不推荐) // 简单的替换可能足够,因为 allTerminalThemes 本身是 ref allTerminalThemes.value[index] = { ...allTerminalThemes.value[index], themeData: fullTheme.themeData }; console.log(`[AppearanceStore] Updated theme data for ${themeId} in local store.`); } else { // 如果列表中不存在(理论上不应发生,因为初始加载了元数据),可以考虑添加到列表 console.warn(`[AppearanceStore] Theme metadata for ${themeId} not found in initial list, but loaded data.`); // allTerminalThemes.value.push(fullTheme); // 或者不添加,仅返回数据 } return fullTheme.themeData; } else { console.error(`[AppearanceStore] Loaded data for theme ${themeId} is invalid or missing themeData.`); return null; } } catch (err: any) { console.error(`加载终端主题 ${themeId} 数据失败:`, err); error.value = err.response?.data?.message || err.message || `加载主题 ${themeId} 数据失败`; return null; // 返回 null 表示加载失败 } } // --- 背景图片 Actions --- /** * 上传页面背景图片 * @param file File 对象 */ async function uploadPageBackground(file: File): Promise { const formData = new FormData(); formData.append('pageBackgroundFile', file); try { const response = await apiClient.post<{ filePath: string }>('/appearance/background/page', formData, { // 使用 apiClient headers: { 'Content-Type': 'multipart/form-data' } }); // 更新本地状态 (虽然 updateAppearanceSettings 也会做,但这里立即反映) appearanceSettings.value.pageBackgroundImage = response.data.filePath; applyPageBackground(); // 应用新背景 return response.data.filePath; } catch (err: any) { console.error('上传页面背景失败:', err); throw new Error(err.response?.data?.message || err.message || '上传页面背景失败'); } } /** * 上传终端背景图片 * @param file File 对象 */ async function uploadTerminalBackground(file: File): Promise { const formData = new FormData(); formData.append('terminalBackgroundFile', file); try { const response = await apiClient.post<{ filePath: string }>('/appearance/background/terminal', formData, { // 使用 apiClient headers: { 'Content-Type': 'multipart/form-data' } }); appearanceSettings.value.terminalBackgroundImage = response.data.filePath; // 终端背景的应用由 Terminal 组件处理 return response.data.filePath; } catch (err: any) { console.error('上传终端背景失败:', err); throw new Error(err.response?.data?.message || err.message || '上传终端背景失败'); } } /** * 移除页面背景 */ async function removePageBackground() { try { // 先调用后端删除接口 await apiClient.delete('/appearance/background/page'); // 成功后再更新数据库记录 await updateAppearanceSettings({ pageBackgroundImage: '' }); } catch (err: any) { console.error('移除页面背景失败:', err); throw new Error(err.response?.data?.message || err.message || '移除页面背景失败'); } } /** * 移除终端背景 */ async function removeTerminalBackground() { try { // 先调用后端删除接口 await apiClient.delete('/appearance/background/terminal'); // 成功后再更新数据库记录 await updateAppearanceSettings({ terminalBackgroundImage: '' }); } catch (err: any) { console.error('移除终端背景失败:', err); throw new Error(err.response?.data?.message || err.message || '移除终端背景失败'); } } // --- Helper Functions --- /** * 将 UI 主题 (CSS 变量) 应用到文档根元素。 * @param theme 要应用的 UI 主题对象。 */ function applyUiTheme(theme: Record) { const root = document.documentElement; // 先移除可能存在的旧变量(可选,但更干净) // Object.keys(defaultUiTheme).forEach(key => root.style.removeProperty(key)); // 应用新变量 for (const [key, value] of Object.entries(theme)) { root.style.setProperty(key, value); } } /** * 应用页面背景设置到 body 元素 */ function applyPageBackground() { const body = document.body; if (pageBackgroundImage.value) { // --- 修改开始:使用 URL 构造函数改进 URL 拼接 --- const backendUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin; // 如果未设置 VITE_API_BASE_URL,则回退到当前页面源 const imagePath = pageBackgroundImage.value; console.log(`[AppearanceStore applyPageBackground] Base URL: "${backendUrl}", Image Path: "${imagePath}"`); let fullImageUrl = ''; try { // 假设 imagePath 是相对于后端根目录的路径 (例如 /uploads/image.jpg) // 使用 URL 构造函数确保路径正确拼接 const baseUrl = new URL(backendUrl); // 确保 imagePath 是以 / 开头,如果不是则添加 const correctedPath = imagePath.startsWith('/') ? imagePath : `/${imagePath}`; fullImageUrl = new URL(correctedPath, baseUrl).href; console.log(`[AppearanceStore applyPageBackground] Constructed Full Image URL: "${fullImageUrl}"`); } catch (e) { console.error(`[AppearanceStore applyPageBackground] Error constructing image URL:`, e); // URL 构建失败,清除背景并退出 body.style.backgroundImage = 'none'; return; // 停止执行 } // --- 修改结束 --- // 应用背景图片 // 先设置为空,强制浏览器重新请求(可能有助于避免缓存问题) body.style.backgroundImage = 'none'; // 使用 nextTick 确保 DOM 更新后再设置背景 nextTick(() => { // 再次检查 fullImageUrl 是否有效 if (fullImageUrl) { body.style.backgroundImage = `url(${fullImageUrl})`; body.style.backgroundSize = 'cover'; // 覆盖整个区域 body.style.backgroundPosition = 'center'; // 居中显示 body.style.backgroundRepeat = 'no-repeat'; // 不重复 body.style.backgroundAttachment = 'fixed'; // 背景固定,不随滚动条滚动 (可选) console.log(`[AppearanceStore applyPageBackground] Applied background image: ${fullImageUrl}`); } else { console.warn(`[AppearanceStore applyPageBackground] Skipping background application due to invalid URL.`); body.style.backgroundImage = 'none'; // 确保清除 } }); } else { // 如果没有设置背景图片,则清除背景 body.style.backgroundImage = 'none'; console.log(`[AppearanceStore applyPageBackground] Cleared background image.`); } // 注意:直接设置 body 透明度会影响所有子元素,通常不建议。 // 如果需要背景透明效果,通常结合伪元素或额外 div 实现。 // 这里暂时不直接应用 pageBackgroundOpacity 到 body。 console.log('[AppearanceStore] 页面背景已应用:', pageBackgroundImage.value); } // --- Watchers --- // 监听 UI 主题变化并应用 watch(currentUiTheme, (newTheme) => { applyUiTheme(newTheme); }, { deep: true, immediate: true }); // 添加 immediate: true 确保初始加载时应用默认主题 // 监听页面背景变化并应用 watch(pageBackgroundImage, () => { // 只监听图片变化 applyPageBackground(); }); return { isLoading, error, // State refs (原始数据) appearanceSettings, allTerminalThemes, // 导出重命名后的 ref // Computed Getters currentUiTheme, activeTerminalThemeId, currentTerminalTheme, currentTerminalFontFamily, currentTerminalFontSize, currentEditorFontSize, // <-- 新增 pageBackgroundImage, // pageBackgroundOpacity, // Removed terminalBackgroundImage, // terminalBackgroundOpacity, // Removed // Actions loadInitialAppearanceData, updateAppearanceSettings, saveCustomUiTheme, resetCustomUiTheme, setActiveTerminalTheme, setTerminalFontFamily, setTerminalFontSize, setEditorFontSize, // <-- 新增 setTerminalBackgroundEnabled, // <-- 新增 createTerminalTheme, // 保留 updateTerminalTheme, // 保留 deleteTerminalTheme, // 保留 importTerminalTheme, // 保留 exportTerminalTheme, uploadPageBackground, uploadTerminalBackground, // setPageBackgroundOpacity, // Removed // setTerminalBackgroundOpacity, // Removed removePageBackground, removeTerminalBackground, loadTerminalThemeData, // <-- 新增导出 isTerminalBackgroundEnabled, // <-- 新增导出 // Visibility control isStyleCustomizerVisible, toggleStyleCustomizer, }; });