update
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import axios from 'axios';
|
||||
import { ref, computed, watch, nextTick } from 'vue'; // 导入 nextTick
|
||||
import type { ITheme } from 'xterm';
|
||||
import type { TerminalTheme } from '../../../backend/src/types/terminal-theme.types'; // 引用后端类型
|
||||
import type { AppearanceSettings, UpdateAppearanceDto } from '../../../backend/src/types/appearance.types'; // 引用后端类型
|
||||
import { defaultXtermTheme, defaultUiTheme } from './default-themes.js'; // 尝试添加 .js (编译后) 或保持 .ts
|
||||
|
||||
// Helper function to safely parse JSON
|
||||
const safeJsonParse = <T>(jsonString: string | undefined | null, defaultValue: T): T => {
|
||||
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<string | null>(null);
|
||||
const isStyleCustomizerVisible = ref(false); // 新增:控制样式编辑器可见性
|
||||
|
||||
// Appearance Settings State
|
||||
const appearanceSettings = ref<Partial<AppearanceSettings>>({}); // 从 API 获取的原始设置
|
||||
const availableTerminalThemes = ref<TerminalTheme[]>([]); // 终端主题列表
|
||||
|
||||
// --- Computed Properties (Getters) ---
|
||||
|
||||
// 当前应用的 UI 主题 (CSS 变量对象)
|
||||
const currentUiTheme = computed<Record<string, string>>(() => {
|
||||
return safeJsonParse(appearanceSettings.value.customUiTheme, defaultUiTheme);
|
||||
});
|
||||
|
||||
// 当前激活的终端主题 ID
|
||||
const activeTerminalThemeId = computed(() => appearanceSettings.value.activeTerminalThemeId);
|
||||
|
||||
// 当前应用的终端主题对象 (ITheme)
|
||||
const currentTerminalTheme = computed<ITheme>(() => {
|
||||
if (!activeTerminalThemeId.value || availableTerminalThemes.value.length === 0) {
|
||||
return defaultXtermTheme; // 回退到默认
|
||||
}
|
||||
const activeTheme = availableTerminalThemes.value.find(t => t._id === activeTerminalThemeId.value);
|
||||
return activeTheme ? activeTheme.themeData : defaultXtermTheme; // 找不到也回退
|
||||
});
|
||||
|
||||
// 当前终端字体设置
|
||||
const currentTerminalFontFamily = computed<string>(() => {
|
||||
return appearanceSettings.value.terminalFontFamily || 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"'; // 提供默认值
|
||||
});
|
||||
|
||||
// 页面背景图片 URL
|
||||
const pageBackgroundImage = computed(() => appearanceSettings.value.pageBackgroundImage);
|
||||
// 页面背景透明度
|
||||
const pageBackgroundOpacity = computed(() => appearanceSettings.value.pageBackgroundOpacity ?? 1.0); // 默认 1
|
||||
|
||||
// 终端背景图片 URL
|
||||
const terminalBackgroundImage = computed(() => appearanceSettings.value.terminalBackgroundImage);
|
||||
// 终端背景透明度
|
||||
const terminalBackgroundOpacity = computed(() => appearanceSettings.value.terminalBackgroundOpacity ?? 1.0); // 默认 1
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
/**
|
||||
* 加载所有外观相关设置 (外观设置 + 终端主题列表)
|
||||
*/
|
||||
async function loadInitialAppearanceData() {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
// 并行加载外观设置和主题列表
|
||||
const [settingsResponse, themesResponse] = await Promise.all([
|
||||
axios.get<AppearanceSettings>('/api/v1/appearance'),
|
||||
axios.get<TerminalTheme[]>('/api/v1/terminal-themes')
|
||||
]);
|
||||
appearanceSettings.value = settingsResponse.data;
|
||||
availableTerminalThemes.value = themesResponse.data;
|
||||
console.log('[AppearanceStore] 外观设置已加载:', appearanceSettings.value);
|
||||
console.log('[AppearanceStore] 终端主题列表已加载:', availableTerminalThemes.value);
|
||||
|
||||
// 应用加载的 UI 主题
|
||||
applyUiTheme(currentUiTheme.value);
|
||||
// 应用背景
|
||||
applyPageBackground();
|
||||
// 终端背景和主题将在 Terminal 组件中应用
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('加载外观数据失败:', err);
|
||||
error.value = err.response?.data?.message || err.message || '加载外观数据失败';
|
||||
// 出错时应用默认值
|
||||
appearanceSettings.value = {}; // 清空可能不完整的设置
|
||||
availableTerminalThemes.value = [];
|
||||
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 要更新的设置项
|
||||
*/
|
||||
async function updateAppearanceSettings(updates: UpdateAppearanceDto) {
|
||||
try {
|
||||
const response = await axios.put<AppearanceSettings>('/api/v1/appearance', updates);
|
||||
// 使用后端返回的最新设置更新本地状态
|
||||
appearanceSettings.value = response.data;
|
||||
console.log('[AppearanceStore] 外观设置已更新:', appearanceSettings.value);
|
||||
// 如果 UI 主题或背景更新,重新应用
|
||||
if (updates.customUiTheme !== undefined) applyUiTheme(currentUiTheme.value);
|
||||
if (updates.pageBackgroundImage !== undefined || updates.pageBackgroundOpacity !== undefined) applyPageBackground();
|
||||
// 终端相关设置由 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<string, string>) {
|
||||
await updateAppearanceSettings({ customUiTheme: JSON.stringify(uiTheme) });
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置为默认 UI 主题并保存。
|
||||
*/
|
||||
async function resetCustomUiTheme() {
|
||||
await saveCustomUiTheme(defaultUiTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置激活的终端主题
|
||||
* @param themeId 主题 ID
|
||||
*/
|
||||
async function setActiveTerminalTheme(themeId: string | null) {
|
||||
await updateAppearanceSettings({ activeTerminalThemeId: themeId ?? undefined });
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置终端字体
|
||||
* @param fontFamily 字体列表字符串
|
||||
*/
|
||||
async function setTerminalFontFamily(fontFamily: string) {
|
||||
await updateAppearanceSettings({ terminalFontFamily: fontFamily });
|
||||
}
|
||||
|
||||
// --- 终端主题列表管理 Actions ---
|
||||
|
||||
/**
|
||||
* 重新加载终端主题列表
|
||||
*/
|
||||
async function reloadTerminalThemes() {
|
||||
try {
|
||||
const response = await axios.get<TerminalTheme[]>('/api/v1/terminal-themes');
|
||||
availableTerminalThemes.value = response.data;
|
||||
} catch (err: any) {
|
||||
console.error('重新加载终端主题列表失败:', err);
|
||||
// 可以选择抛出错误或显示通知
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的终端主题
|
||||
* @param name 主题名称
|
||||
* @param themeData 主题数据 (ITheme)
|
||||
*/
|
||||
async function createTerminalTheme(name: string, themeData: ITheme) {
|
||||
try {
|
||||
await axios.post('/api/v1/terminal-themes', { name, themeData });
|
||||
await reloadTerminalThemes(); // 重新加载列表
|
||||
} 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 axios.put(`/api/v1/terminal-themes/${id}`, { name, themeData });
|
||||
await reloadTerminalThemes(); // 重新加载列表
|
||||
} 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 axios.delete(`/api/v1/terminal-themes/${id}`);
|
||||
// 如果删除的是当前激活的主题,则切换回默认
|
||||
if (activeTerminalThemeId.value === id) {
|
||||
await setActiveTerminalTheme(null); // 或者设置为默认主题的 ID
|
||||
}
|
||||
await reloadTerminalThemes(); // 重新加载列表
|
||||
} 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 axios.post('/api/v1/terminal-themes/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
await reloadTerminalThemes();
|
||||
} 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 axios.get(`/api/v1/terminal-themes/${id}/export`, {
|
||||
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 || '导出终端主题失败');
|
||||
}
|
||||
}
|
||||
|
||||
// --- 背景图片 Actions ---
|
||||
/**
|
||||
* 上传页面背景图片
|
||||
* @param file File 对象
|
||||
*/
|
||||
async function uploadPageBackground(file: File): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('pageBackgroundFile', file);
|
||||
try {
|
||||
const response = await axios.post<{ filePath: string }>('/api/v1/appearance/background/page', formData, {
|
||||
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<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('terminalBackgroundFile', file);
|
||||
try {
|
||||
const response = await axios.post<{ filePath: string }>('/api/v1/appearance/background/terminal', formData, {
|
||||
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 || '上传终端背景失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置页面背景透明度
|
||||
* @param opacity 0-1 之间的数字
|
||||
*/
|
||||
async function setPageBackgroundOpacity(opacity: number) {
|
||||
await updateAppearanceSettings({ pageBackgroundOpacity: opacity });
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置终端背景透明度
|
||||
* @param opacity 0-1 之间的数字
|
||||
*/
|
||||
async function setTerminalBackgroundOpacity(opacity: number) {
|
||||
await updateAppearanceSettings({ terminalBackgroundOpacity: opacity });
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除页面背景
|
||||
*/
|
||||
async function removePageBackground() {
|
||||
await updateAppearanceSettings({ pageBackgroundImage: '' }); // 设置为空字符串或其他表示移除的值
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除终端背景
|
||||
*/
|
||||
async function removeTerminalBackground() {
|
||||
await updateAppearanceSettings({ terminalBackgroundImage: '' });
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Functions ---
|
||||
/**
|
||||
* 将 UI 主题 (CSS 变量) 应用到文档根元素。
|
||||
* @param theme 要应用的 UI 主题对象。
|
||||
*/
|
||||
function applyUiTheme(theme: Record<string, string>) {
|
||||
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);
|
||||
}
|
||||
console.log('[AppearanceStore] UI 主题已应用:', theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用页面背景设置到 body 元素
|
||||
*/
|
||||
function applyPageBackground() {
|
||||
const body = document.body;
|
||||
if (pageBackgroundImage.value) {
|
||||
// --- 修改开始 ---
|
||||
// 使用环境变量获取后端基础 URL
|
||||
const backendUrl = import.meta.env.VITE_API_BASE_URL || ''; // 提供一个默认空字符串以防万一
|
||||
const imagePath = pageBackgroundImage.value;
|
||||
console.log(`[AppearanceStore applyPageBackground] backendUrl: "${backendUrl}", imagePath: "${imagePath}"`); // 详细日志
|
||||
const fullImageUrl = `${backendUrl}${imagePath}`;
|
||||
console.log(`[AppearanceStore applyPageBackground] fullImageUrl: "${fullImageUrl}"`); // 打印完整 URL
|
||||
// --- 修改结束 ---
|
||||
|
||||
// Use the full URL
|
||||
// 先设置为空,强制更新
|
||||
body.style.backgroundImage = 'none';
|
||||
// 在下一个 tick 中设置图片,尝试解决时序问题
|
||||
nextTick(() => {
|
||||
body.style.backgroundImage = `url(${fullImageUrl})`;
|
||||
body.style.backgroundSize = 'cover';
|
||||
body.style.backgroundPosition = 'center';
|
||||
body.style.backgroundRepeat = 'no-repeat';
|
||||
});
|
||||
// 可以考虑添加透明度处理,例如通过伪元素
|
||||
} else {
|
||||
body.style.backgroundImage = 'none';
|
||||
}
|
||||
// 注意:直接设置 body 透明度会影响所有子元素,通常不建议。
|
||||
// 如果需要背景透明效果,通常结合伪元素或额外 div 实现。
|
||||
// 这里暂时不直接应用 pageBackgroundOpacity 到 body。
|
||||
console.log('[AppearanceStore] 页面背景已应用:', pageBackgroundImage.value);
|
||||
}
|
||||
|
||||
// --- Watchers ---
|
||||
// 监听 UI 主题变化并应用
|
||||
watch(currentUiTheme, (newTheme) => {
|
||||
applyUiTheme(newTheme);
|
||||
}, { deep: true });
|
||||
|
||||
// 监听页面背景变化并应用
|
||||
watch([pageBackgroundImage, pageBackgroundOpacity], () => {
|
||||
applyPageBackground();
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
// State refs (原始数据)
|
||||
appearanceSettings,
|
||||
availableTerminalThemes,
|
||||
// Computed Getters
|
||||
currentUiTheme,
|
||||
activeTerminalThemeId,
|
||||
currentTerminalTheme,
|
||||
currentTerminalFontFamily,
|
||||
pageBackgroundImage,
|
||||
pageBackgroundOpacity,
|
||||
terminalBackgroundImage,
|
||||
terminalBackgroundOpacity,
|
||||
// Actions
|
||||
loadInitialAppearanceData,
|
||||
updateAppearanceSettings,
|
||||
saveCustomUiTheme,
|
||||
resetCustomUiTheme,
|
||||
setActiveTerminalTheme,
|
||||
setTerminalFontFamily,
|
||||
reloadTerminalThemes,
|
||||
createTerminalTheme,
|
||||
updateTerminalTheme,
|
||||
deleteTerminalTheme,
|
||||
importTerminalTheme,
|
||||
exportTerminalTheme,
|
||||
uploadPageBackground,
|
||||
uploadTerminalBackground,
|
||||
setPageBackgroundOpacity,
|
||||
setTerminalBackgroundOpacity,
|
||||
removePageBackground,
|
||||
removeTerminalBackground,
|
||||
// Visibility control
|
||||
isStyleCustomizerVisible,
|
||||
toggleStyleCustomizer,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ITheme } from 'xterm';
|
||||
|
||||
// 默认 xterm 主题
|
||||
// (与 backend/src/config/default-themes.ts 中的定义保持一致)
|
||||
export const defaultXtermTheme: ITheme = {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#d4d4d4',
|
||||
selectionBackground: '#264f78', // 使用 selectionBackground
|
||||
black: '#000000',
|
||||
red: '#cd3131',
|
||||
green: '#0dbc79',
|
||||
yellow: '#e5e510',
|
||||
blue: '#2472c8',
|
||||
magenta: '#bc3fbc',
|
||||
cyan: '#11a8cd',
|
||||
white: '#e5e5e5',
|
||||
brightBlack: '#666666',
|
||||
brightRed: '#f14c4c',
|
||||
brightGreen: '#23d18b',
|
||||
brightYellow: '#f5f543',
|
||||
brightBlue: '#3b8eea',
|
||||
brightMagenta: '#d670d6',
|
||||
brightCyan: '#29b8db',
|
||||
brightWhite: '#e5e5e5'
|
||||
};
|
||||
|
||||
// 默认 UI 主题 (CSS 变量)
|
||||
// (与 backend/src/config/default-themes.ts 中的定义保持一致)
|
||||
export const defaultUiTheme: Record<string, string> = {
|
||||
'--app-bg-color': '#ffffff',
|
||||
'--text-color': '#333333',
|
||||
'--text-color-secondary': '#666666',
|
||||
'--border-color': '#cccccc',
|
||||
'--link-color': '#333',
|
||||
'--link-hover-color': '#0056b3',
|
||||
'--link-active-color': '#007bff',
|
||||
'--header-bg-color': '#f0f0f0',
|
||||
'--footer-bg-color': '#f0f0f0',
|
||||
'--button-bg-color': '#007bff',
|
||||
'--button-text-color': '#ffffff',
|
||||
'--button-hover-bg-color': '#0056b3',
|
||||
'--font-family-sans-serif': 'sans-serif',
|
||||
'--base-padding': '1rem',
|
||||
'--base-margin': '0.5rem',
|
||||
};
|
||||
@@ -1,305 +1,203 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import axios from 'axios';
|
||||
import { ref, computed, watch } from 'vue'; // Import computed and watch
|
||||
import { ref, computed } from 'vue'; // 移除 watch
|
||||
import i18n, { setLocale, defaultLng } from '../i18n'; // Import i18n instance and setLocale
|
||||
import type { ITheme } from 'xterm'; // 导入 xterm 主题类型
|
||||
// 移除 ITheme 和默认主题定义,这些移到 appearance.store.ts
|
||||
|
||||
// Define the type for settings state explicitly
|
||||
// 定义通用设置状态类型
|
||||
interface SettingsState {
|
||||
language: 'en' | 'zh';
|
||||
ipWhitelist: string;
|
||||
maxLoginAttempts: string;
|
||||
loginBanDuration: string;
|
||||
showPopupFileEditor: string; // 弹窗编辑器设置
|
||||
shareFileEditorTabs?: string; // 共享编辑器标签页设置 ('true'/'false')
|
||||
customUiTheme?: string; // UI 主题 (CSS 变量 JSON 字符串)
|
||||
customXtermTheme?: string; // xterm 主题 (JSON 字符串)
|
||||
// Add other settings keys here as needed
|
||||
[key: string]: string | undefined; // Allow other string settings, make value optional
|
||||
language?: 'en' | 'zh'; // 语言现在是可选的,因为可能在 appearance store 中处理
|
||||
ipWhitelist?: string;
|
||||
maxLoginAttempts?: string;
|
||||
loginBanDuration?: string;
|
||||
showPopupFileEditor?: string; // 'true' or 'false'
|
||||
shareFileEditorTabs?: string; // 'true' or 'false'
|
||||
ipWhitelistEnabled?: string; // 添加 IP 白名单启用状态 'true' or 'false'
|
||||
// Add other general settings keys here as needed
|
||||
[key: string]: string | undefined; // Allow other string settings
|
||||
}
|
||||
|
||||
// 默认 UI 主题 (CSS 变量)
|
||||
const defaultUiTheme: Record<string, string> = {
|
||||
'--app-bg-color': '#ffffff',
|
||||
'--text-color': '#333333',
|
||||
'--text-color-secondary': '#666666',
|
||||
'--border-color': '#cccccc',
|
||||
'--link-color': '#333',
|
||||
'--link-hover-color': '#0056b3',
|
||||
'--link-active-color': '#007bff',
|
||||
'--header-bg-color': '#f0f0f0',
|
||||
'--footer-bg-color': '#f0f0f0',
|
||||
'--button-bg-color': '#007bff',
|
||||
'--button-text-color': '#ffffff',
|
||||
'--button-hover-bg-color': '#0056b3',
|
||||
'--font-family-sans-serif': 'sans-serif',
|
||||
'--base-padding': '1rem',
|
||||
'--base-margin': '0.5rem',
|
||||
};
|
||||
|
||||
// 默认 xterm 主题
|
||||
const defaultXtermTheme: ITheme = {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#d4d4d4',
|
||||
selectionBackground: '#264f78', // 使用 selectionBackground 而不是 selection
|
||||
black: '#000000',
|
||||
red: '#cd3131',
|
||||
green: '#0dbc79',
|
||||
yellow: '#e5e510',
|
||||
blue: '#2472c8',
|
||||
magenta: '#bc3fbc',
|
||||
cyan: '#11a8cd',
|
||||
white: '#e5e5e5',
|
||||
brightBlack: '#666666',
|
||||
brightRed: '#f14c4c',
|
||||
brightGreen: '#23d18b',
|
||||
brightYellow: '#f5f543',
|
||||
brightBlue: '#3b8eea',
|
||||
brightMagenta: '#d670d6',
|
||||
brightCyan: '#29b8db',
|
||||
brightWhite: '#e5e5e5'
|
||||
};
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
// --- State ---
|
||||
const settings = ref<Partial<SettingsState>>({}); // Use Partial initially
|
||||
const settings = ref<Partial<SettingsState>>({}); // 通用设置状态
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const isStyleCustomizerVisible = ref(false); // 控制样式编辑器可见性
|
||||
const currentUiTheme = ref<Record<string, string>>({ ...defaultUiTheme }); // 当前应用的 UI 主题
|
||||
const currentXtermTheme = ref<ITheme>({ ...defaultXtermTheme }); // 当前应用的 xterm 主题
|
||||
// 移除外观相关状态: isStyleCustomizerVisible, currentUiTheme, currentXtermTheme
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
/**
|
||||
* Fetches all settings from the backend and updates the store state.
|
||||
* Fetches general settings from the backend and updates the store state.
|
||||
* Also sets the i18n locale based on the fetched language setting.
|
||||
* Should be called early in the application lifecycle (e.g., main.ts).
|
||||
*/
|
||||
async function loadInitialSettings() {
|
||||
console.log('[SettingsStore] Entering loadInitialSettings function...'); // <-- 添加更早的日志
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
let fetchedLang: 'en' | 'zh' | undefined;
|
||||
|
||||
try {
|
||||
console.log('[SettingsStore] Starting loadInitialSettings...'); // 添加日志
|
||||
console.log('[SettingsStore] 加载通用设置...');
|
||||
const response = await axios.get<Record<string, string>>('/api/v1/settings');
|
||||
settings.value = response.data; // Store all fetched settings
|
||||
console.log('[SettingsStore] Fetched settings raw:', JSON.stringify(response.data)); // 打印原始响应
|
||||
console.log('[SettingsStore] Raw showPopupFileEditor from backend:', response.data.showPopupFileEditor);
|
||||
settings.value = response.data; // Store fetched general settings
|
||||
console.log('[SettingsStore] 通用设置已加载:', settings.value);
|
||||
|
||||
// --- 设置默认值 (如果后端未返回) ---
|
||||
// 弹窗编辑器设置 (保持不变)
|
||||
if (settings.value.showPopupFileEditor === undefined) {
|
||||
console.log('[SettingsStore] showPopupFileEditor is undefined, setting default: true');
|
||||
settings.value.showPopupFileEditor = 'true';
|
||||
}
|
||||
// 共享编辑器标签页设置 (保持不变)
|
||||
if (settings.value.shareFileEditorTabs === undefined) {
|
||||
console.log('[SettingsStore] Setting default for shareFileEditorTabs: true');
|
||||
settings.value.shareFileEditorTabs = 'true';
|
||||
}
|
||||
if (settings.value.ipWhitelistEnabled === undefined) {
|
||||
settings.value.ipWhitelistEnabled = 'false'; // 默认禁用 IP 白名单
|
||||
}
|
||||
if (settings.value.maxLoginAttempts === undefined) {
|
||||
settings.value.maxLoginAttempts = '5'; // 默认 5 次
|
||||
}
|
||||
if (settings.value.loginBanDuration === undefined) {
|
||||
settings.value.loginBanDuration = '300'; // 默认 300 秒
|
||||
}
|
||||
|
||||
// --- 加载自定义主题 ---
|
||||
loadAndApplyThemesFromSettings(); // 新增:加载并应用主题
|
||||
|
||||
// --- 语言设置 (保持不变) ---
|
||||
// Determine and apply language
|
||||
// --- 语言设置 ---
|
||||
const langFromSettings = settings.value.language;
|
||||
if (langFromSettings === 'en' || langFromSettings === 'zh') {
|
||||
fetchedLang = langFromSettings;
|
||||
} else {
|
||||
// Fallback logic if setting is missing or invalid
|
||||
const navigatorLang = navigator.language?.split('-')[0];
|
||||
fetchedLang = navigatorLang === 'zh' ? 'zh' : defaultLng; // Use browser lang or default
|
||||
console.warn(`[SettingsStore] Language setting not found or invalid ('${langFromSettings}'). Falling back to '${fetchedLang}'.`);
|
||||
// Optionally save the fallback language back to the backend if desired
|
||||
fetchedLang = navigatorLang === 'zh' ? 'zh' : defaultLng;
|
||||
console.warn(`[SettingsStore] 语言设置无效 ('${langFromSettings}'), 回退到 '${fetchedLang}'.`);
|
||||
// Optionally save the fallback language back
|
||||
// await updateSetting('language', fetchedLang);
|
||||
}
|
||||
|
||||
// Ensure fetchedLang is valid before calling setLocale (保持不变)
|
||||
if (fetchedLang) {
|
||||
console.log(`[SettingsStore] Determined language: ${fetchedLang}. Applying locale...`);
|
||||
console.log(`[SettingsStore] 设置语言: ${fetchedLang}`);
|
||||
setLocale(fetchedLang);
|
||||
} else {
|
||||
console.error('[SettingsStore] Could not determine a valid language to set.');
|
||||
console.error('[SettingsStore] 无法确定有效语言。');
|
||||
setLocale(defaultLng);
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load initial settings:', err);
|
||||
error.value = err.response?.data?.message || err.message || 'Failed to load settings';
|
||||
// Apply default language on error
|
||||
setLocale(defaultLng);
|
||||
console.error('加载通用设置失败:', err);
|
||||
error.value = err.response?.data?.message || err.message || '加载设置失败';
|
||||
setLocale(defaultLng); // 出错时使用默认语言
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 settings ref 加载主题设置,解析并应用它们。
|
||||
*/
|
||||
function loadAndApplyThemesFromSettings() {
|
||||
// 加载 UI 主题
|
||||
try {
|
||||
if (settings.value.customUiTheme) {
|
||||
const parsedUiTheme = JSON.parse(settings.value.customUiTheme);
|
||||
// 合并默认值,确保所有变量都存在
|
||||
currentUiTheme.value = { ...defaultUiTheme, ...parsedUiTheme };
|
||||
} else {
|
||||
currentUiTheme.value = { ...defaultUiTheme }; // 使用默认值
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[SettingsStore] Failed to parse custom UI theme, using default:', e);
|
||||
currentUiTheme.value = { ...defaultUiTheme };
|
||||
}
|
||||
|
||||
// 加载 xterm 主题
|
||||
try {
|
||||
if (settings.value.customXtermTheme) {
|
||||
const parsedXtermTheme = JSON.parse(settings.value.customXtermTheme);
|
||||
// 合并默认值
|
||||
currentXtermTheme.value = { ...defaultXtermTheme, ...parsedXtermTheme };
|
||||
} else {
|
||||
currentXtermTheme.value = { ...defaultXtermTheme }; // 使用默认值
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[SettingsStore] Failed to parse custom xterm theme, using default:', e);
|
||||
currentXtermTheme.value = { ...defaultXtermTheme };
|
||||
}
|
||||
|
||||
// 应用加载的主题
|
||||
applyUiTheme(currentUiTheme.value);
|
||||
// xterm 主题的应用将在 Terminal 组件内部通过 watch 监听 currentXtermTheme 实现
|
||||
}
|
||||
// 移除外观相关函数: loadAndApplyThemesFromSettings, applyUiTheme, saveCustomThemes, resetCustomThemes, toggleStyleCustomizer
|
||||
|
||||
/**
|
||||
* 将 UI 主题 (CSS 变量) 应用到文档根元素。
|
||||
* @param theme 要应用的 UI 主题对象。
|
||||
*/
|
||||
function applyUiTheme(theme: Record<string, string>) {
|
||||
const root = document.documentElement;
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
root.style.setProperty(key, value);
|
||||
}
|
||||
console.log('[SettingsStore] Applied UI theme:', theme);
|
||||
}
|
||||
|
||||
// 监听 currentUiTheme 的变化并自动应用
|
||||
watch(currentUiTheme, (newTheme) => {
|
||||
applyUiTheme(newTheme);
|
||||
}, { deep: true }); // 使用 deep watch 监听对象内部变化
|
||||
|
||||
/**
|
||||
* Updates a single setting value both locally and on the backend.
|
||||
* Updates a single general setting value both locally and on the backend.
|
||||
* @param key The setting key to update.
|
||||
* @param value The new value for the setting.
|
||||
*/
|
||||
async function updateSetting(key: keyof SettingsState, value: string) {
|
||||
// 移除外观相关的键检查
|
||||
const allowedKeys: Array<keyof SettingsState> = [
|
||||
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
||||
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled'
|
||||
];
|
||||
if (!allowedKeys.includes(key)) {
|
||||
console.error(`[SettingsStore] 尝试更新不允许的设置键: ${key}`);
|
||||
throw new Error(`不允许更新设置项 '${key}'`);
|
||||
}
|
||||
|
||||
try {
|
||||
// 注意:后端 controller 现在会过滤,但前端也做一层检查更好
|
||||
await axios.put('/api/v1/settings', { [key]: value });
|
||||
// Update store state *after* successful API call
|
||||
settings.value = { ...settings.value, [key]: value };
|
||||
// 如果更新的是主题设置,需要重新解析和应用
|
||||
if (key === 'customUiTheme' || key === 'customXtermTheme') {
|
||||
loadAndApplyThemesFromSettings();
|
||||
}
|
||||
|
||||
// If updating language, also update i18n
|
||||
if (key === 'language' && (value === 'en' || value === 'zh')) {
|
||||
setLocale(value);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`Failed to update setting '${key}':`, err);
|
||||
throw new Error(err.response?.data?.message || err.message || `Failed to update setting '${key}'`);
|
||||
console.error(`更新设置项 '${key}' 失败:`, err);
|
||||
throw new Error(err.response?.data?.message || err.message || `更新设置项 '${key}' 失败`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates multiple settings values both locally and on the backend.
|
||||
* Updates multiple general settings values both locally and on the backend.
|
||||
* @param updates An object containing key-value pairs of settings to update.
|
||||
*/
|
||||
async function updateMultipleSettings(updates: Partial<SettingsState>) {
|
||||
// 移除外观相关的键检查
|
||||
const allowedKeys: Array<keyof SettingsState> = [
|
||||
'language', 'ipWhitelist', 'maxLoginAttempts', 'loginBanDuration',
|
||||
'showPopupFileEditor', 'shareFileEditorTabs', 'ipWhitelistEnabled'
|
||||
];
|
||||
const filteredUpdates: Partial<SettingsState> = {};
|
||||
let languageUpdate: 'en' | 'zh' | undefined = undefined;
|
||||
|
||||
for (const key in updates) {
|
||||
if (allowedKeys.includes(key as keyof SettingsState)) {
|
||||
filteredUpdates[key as keyof SettingsState] = updates[key];
|
||||
if (key === 'language' && (updates[key] === 'en' || updates[key] === 'zh')) {
|
||||
languageUpdate = updates[key] as 'en' | 'zh';
|
||||
}
|
||||
} else {
|
||||
console.warn(`[SettingsStore] 尝试批量更新不允许的设置键: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(filteredUpdates).length === 0) {
|
||||
console.log('[SettingsStore] 没有有效的通用设置需要更新。');
|
||||
return; // 没有有效设置需要更新
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.put('/api/v1/settings', updates);
|
||||
// 注意:后端 controller 现在会过滤,但前端也做一层检查更好
|
||||
await axios.put('/api/v1/settings', filteredUpdates);
|
||||
// Update store state *after* successful API call
|
||||
settings.value = { ...settings.value, ...updates };
|
||||
// 如果更新包含主题设置,需要重新解析和应用
|
||||
if (updates.customUiTheme !== undefined || updates.customXtermTheme !== undefined) {
|
||||
loadAndApplyThemesFromSettings();
|
||||
}
|
||||
settings.value = { ...settings.value, ...filteredUpdates };
|
||||
|
||||
// If language is updated, apply it
|
||||
if (updates.language && (updates.language === 'en' || updates.language === 'zh')) {
|
||||
setLocale(updates.language);
|
||||
if (languageUpdate) {
|
||||
setLocale(languageUpdate);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to update multiple settings:', err);
|
||||
throw new Error(err.response?.data?.message || err.message || 'Failed to update settings');
|
||||
console.error('批量更新设置失败:', err);
|
||||
throw new Error(err.response?.data?.message || err.message || '批量更新设置失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前编辑器中的自定义主题到后端。
|
||||
* @param uiTheme UI 主题对象
|
||||
* @param xtermTheme xterm 主题对象
|
||||
*/
|
||||
async function saveCustomThemes(uiTheme: Record<string, string>, xtermTheme: ITheme) {
|
||||
const updates: Partial<SettingsState> = {
|
||||
customUiTheme: JSON.stringify(uiTheme),
|
||||
customXtermTheme: JSON.stringify(xtermTheme),
|
||||
};
|
||||
// 更新本地状态以立即反映(虽然 watch 也会触发应用,但这里更新 state 是必要的)
|
||||
currentUiTheme.value = { ...uiTheme };
|
||||
currentXtermTheme.value = { ...xtermTheme };
|
||||
// 调用 updateMultipleSettings 保存到后端
|
||||
await updateMultipleSettings(updates);
|
||||
}
|
||||
// 移除外观相关 actions: saveCustomThemes, resetCustomThemes, toggleStyleCustomizer
|
||||
|
||||
/**
|
||||
* 重置为默认主题并保存。
|
||||
*/
|
||||
async function resetCustomThemes() {
|
||||
await saveCustomThemes(defaultUiTheme, defaultXtermTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换样式编辑器面板的可见性。
|
||||
* @param visible 可选,强制设置可见性
|
||||
*/
|
||||
function toggleStyleCustomizer(visible?: boolean) {
|
||||
isStyleCustomizerVisible.value = visible === undefined ? !isStyleCustomizerVisible.value : visible;
|
||||
}
|
||||
|
||||
// --- Getters --- (保持不变)
|
||||
// --- Getters ---
|
||||
const language = computed(() => settings.value.language || defaultLng);
|
||||
|
||||
// Getter for the popup editor setting, returning boolean (保持不变)
|
||||
// Getter for the popup editor setting, returning boolean
|
||||
const showPopupFileEditorBoolean = computed(() => {
|
||||
return settings.value.showPopupFileEditor !== 'false';
|
||||
});
|
||||
|
||||
// Getter for sharing setting, returning boolean (保持不变)
|
||||
// Getter for sharing setting, returning boolean
|
||||
const shareFileEditorTabsBoolean = computed(() => {
|
||||
return settings.value.shareFileEditorTabs !== 'false';
|
||||
});
|
||||
|
||||
// Getter for IP Whitelist enabled status
|
||||
const ipWhitelistEnabled = computed(() => settings.value.ipWhitelistEnabled === 'true');
|
||||
|
||||
|
||||
return {
|
||||
settings, // 原始设置对象 (可能包含字符串化的主题)
|
||||
settings, // 只包含通用设置
|
||||
isLoading,
|
||||
error,
|
||||
language,
|
||||
showPopupFileEditorBoolean,
|
||||
shareFileEditorTabsBoolean,
|
||||
isStyleCustomizerVisible, // 暴露编辑器可见状态
|
||||
currentUiTheme, // 暴露当前应用的 UI 主题对象
|
||||
currentXtermTheme, // 暴露当前应用的 xterm 主题对象
|
||||
ipWhitelistEnabled, // 暴露 IP 白名单启用状态
|
||||
// 移除外观相关的 getters 和 actions
|
||||
loadInitialSettings,
|
||||
updateSetting,
|
||||
updateMultipleSettings,
|
||||
saveCustomThemes, // 暴露保存主题 action
|
||||
resetCustomThemes, // 暴露重置主题 action
|
||||
toggleStyleCustomizer, // 暴露切换编辑器 action
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user