From 62e39dc75c16131feaf0d00e401a9d913d9dcf6b Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:20:55 +0800 Subject: [PATCH] update --- .../src/components/StyleCustomizer.vue | 199 +++++++++++++++++- packages/frontend/src/locales/en.json | 6 +- packages/frontend/src/locales/zh.json | 6 +- .../frontend/src/stores/default-themes.ts | 7 + packages/frontend/src/style.css | 31 +++ packages/frontend/src/views/SettingsView.vue | 2 +- 6 files changed, 247 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/StyleCustomizer.vue b/packages/frontend/src/components/StyleCustomizer.vue index a2e38c2..5c0e2a3 100644 --- a/packages/frontend/src/components/StyleCustomizer.vue +++ b/packages/frontend/src/components/StyleCustomizer.vue @@ -30,6 +30,8 @@ const editableTerminalFontSize = ref(14); const editableEditorFontSize = ref(14); // <-- 新增,编辑器字体大小 // const editablePageBackgroundOpacity = ref(1.0); // Removed // const editableTerminalBackgroundOpacity = ref(1.0); // Removed +const editableUiThemeString = ref(''); // 用于 textarea 绑定 +const themeParseError = ref(null); // 用于显示 JSON 解析错误 // 终端主题管理相关状态 const selectedTerminalThemeId = ref(null); // 下拉框选择的 ID @@ -63,6 +65,21 @@ const initializeEditableState = () => { uploadError.value = null; importError.value = null; saveThemeError.value = null; + themeParseError.value = null; // 初始化解析错误 + // 初始化 textarea 内容 + // Initialize textarea content with user-friendly format + try { + const themeObject = editableUiTheme.value; + if (themeObject && typeof themeObject === 'object' && Object.keys(themeObject).length > 0) { + const lines = Object.entries(themeObject).map(([key, value]) => ` ${key}: ${value}`); + editableUiThemeString.value = lines.join('\n'); + } else { + editableUiThemeString.value = ''; // Empty if no theme + } + } catch (e) { + console.error("初始化 UI 主题字符串失败:", e); + editableUiThemeString.value = ''; // Fallback to empty + } }; onMounted(initializeEditableState); @@ -135,6 +152,132 @@ const handleResetUiTheme = async () => { } }; +// --- Textarea 和 Color Picker 同步 --- + +// 计算属性:将本地编辑的 UI 主题对象格式化为内部键值对字符串(无大括号,无行尾逗号) +const formattedEditableUiThemeJson = computed(() => { + try { + const themeObject = editableUiTheme.value; + if (!themeObject || typeof themeObject !== 'object' || Object.keys(themeObject).length === 0) { + return ''; // Return empty string if no theme or empty + } + // Generate key-value pairs, indented, one per line + // Generate key-value pairs, indented, one per line, without quotes for easier editing + const lines = Object.entries(themeObject).map(([key, value]) => { + // Output key and value directly + return ` ${key}: ${value}`; // Add indentation + }); + // Join with newline + return lines.join('\n'); + } catch (e) { + console.error("序列化可编辑 UI 主题键值对失败:", e); + return ''; // 回退为空字符串 + } +}); + +// 监听计算属性的变化(通常由颜色选择器引起),更新 textarea 的内容 +watch(formattedEditableUiThemeJson, (newJson) => { + // 只有在 textarea 没有聚焦时才更新,避免覆盖用户输入 + // 或者,如果解析错误存在,也允许更新以显示正确格式 + if (document.activeElement?.id !== 'uiThemeTextarea' || themeParseError.value) { + editableUiThemeString.value = newJson; + if (themeParseError.value && document.activeElement?.id !== 'uiThemeTextarea') { + themeParseError.value = null; // 如果外部更改修复了错误,清除错误提示 + } + } +}); + +// 处理 textarea 内容变化(失去焦点时) +// 处理 textarea 内容变化(失去焦点时) +// 处理 textarea 内容变化(失去焦点时) +// 处理 textarea 内容变化(失去焦点时) +// 处理 textarea 内容变化(失去焦点时) +// 处理 textarea 内容变化(失去焦点时) +// 处理 textarea 内容变化(失去焦点时) +const handleUiThemeStringChange = () => { + themeParseError.value = null; // 清除之前的错误 + let inputText = editableUiThemeString.value.trim(); + + // 如果内容为空,则视为空对象 + if (!inputText) { + editableUiTheme.value = {}; + return; + } + + // 准备构建 JSON 字符串 + let jsonStringToParse = inputText + .split('\n') // 按行分割 + .map(line => line.trim()) // 去除每行首尾空格 + .filter(line => line && line.includes(':')) // 过滤空行和不包含冒号的行 + .map(line => { + // 尝试为 key 和 value 添加引号(如果缺少) + const parts = line.split(/:(.*)/s); // 按第一个冒号分割,保留后面的所有内容 + if (parts.length < 2) return null; // 无效行 + + let key = parts[0].trim(); + let value = parts[1].trim(); + + // 为 key 添加引号(如果需要) + // 移除 key 可能存在的引号再用 stringify 包裹 + if (key.startsWith('"') && key.endsWith('"')) { + key = key.slice(1, -1); + } + if (key.startsWith("'") && key.endsWith("'")) { + key = key.slice(1, -1); + } + key = JSON.stringify(key); // 使用 JSON.stringify 保证正确转义 + + // 为 value 添加引号(如果需要,且不是数字/布尔值/null) + // 移除可能的尾随逗号 + if (value.endsWith(',')) { + value = value.slice(0, -1).trim(); + } + // 移除 value 可能存在的引号再判断 + let originalValue = value; + if (value.startsWith('"') && value.endsWith('"')) { + originalValue = value.slice(1, -1); + } else if (value.startsWith("'") && value.endsWith("'")) { + originalValue = value.slice(1, -1); + } + + // 判断是否需要加引号 + if (isNaN(Number(originalValue)) && originalValue !== 'true' && originalValue !== 'false' && originalValue !== 'null') { + value = JSON.stringify(originalValue); // 使用原始未加引号的值进行 stringify + } else { + // 对于数字、布尔值、null,不需要加引号 + value = originalValue; + } + + + return ` ${key}: ${value}`; // 返回带缩进的键值对 + }) + .filter(line => line !== null) // 过滤掉处理失败的行 + .join(',\n'); // 用逗号和换行符连接 + + // 添加外层大括号 + const fullJsonString = `{\n${jsonStringToParse}\n}`; + + try { + const parsedTheme = JSON.parse(fullJsonString); + // 基础验证:确保是对象 + if (typeof parsedTheme !== 'object' || parsedTheme === null || Array.isArray(parsedTheme)) { + throw new Error(t('styleCustomizer.errorInvalidJsonObject')); + } + // 更新本地的 editableUiTheme ref,这将触发颜色选择器的更新 + editableUiTheme.value = parsedTheme; + // 注意:此时尚未保存到后端,用户需要点击“保存 UI 主题”按钮 + } catch (error: any) { + console.error('解析 UI 主题配置失败:', error); + // 尝试提供更具体的错误信息 + let errorMessage = error.message || t('styleCustomizer.errorInvalidJsonConfig'); + if (error instanceof SyntaxError) { + errorMessage = `${t('styleCustomizer.errorJsonSyntax')}: ${error.message}`; // 需要添加翻译 + } + themeParseError.value = errorMessage; + } +}; + + // 保存终端字体 const handleSaveTerminalFont = async () => { try { @@ -436,6 +579,23 @@ const formatXtermLabel = (key: keyof ITheme): string => { class="text-input" /> + +
+

{{ t('styleCustomizer.uiThemeJsonEditorTitle') }}

+

{{ t('styleCustomizer.uiThemeJsonEditorDesc') }}

+
+ + +
+

{{ themeParseError }}

{{ t('styleCustomizer.terminalStyles') }}

@@ -721,6 +881,19 @@ const formatXtermLabel = (key: keyof ITheme): string => { align-items: center; /* Vertically center items in the row */ gap: calc(var(--base-margin) * 1.5); /* Increase gap slightly for better spacing */ } +/* Special class for full-width elements like textarea */ +.form-group.full-width-group, .error-message.full-width-group { + grid-column: 1 / -1; /* Span all columns */ + grid-template-columns: 1fr; /* Single column layout */ + gap: calc(var(--base-margin) / 2); /* Smaller gap for label/textarea */ +} +.form-group.full-width-group label:not(.sr-only) { /* Adjust label if not screen-reader only */ + grid-column: 1 / 2; /* Ensure label stays in its place if visible */ + margin-bottom: calc(var(--base-margin) / 3); +} +.form-group.full-width-group textarea { + grid-column: 1 / 2; /* Ensure textarea stays in its place */ +} /* Adjust grid for rows without a third element (like inline buttons) */ .form-group > *:nth-child(2):last-child { grid-column: 2 / 4; /* Let the second element span if it's the last */ @@ -816,6 +989,27 @@ section[v-if*="isEditingTheme"] .form-group { outline: 0; box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15); /* 调整聚焦阴影 */ } +/* Style for JSON Textarea */ +.json-textarea { + width: 100%; + font-family: var(--font-family-monospace); /* Use monospace font */ + font-size: 0.9em; + line-height: 1.4; + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 0.8rem; + background-color: var(--input-bg-color, var(--app-bg-color)); + color: var(--text-color); + resize: vertical; + min-height: 200px; /* Ensure decent minimum height */ + box-sizing: border-box; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} +.json-textarea:focus { + border-color: var(--link-active-color); + outline: 0; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15); +} /* Inline buttons within form-group */ @@ -1070,7 +1264,10 @@ hr { font-size: 0.9rem; width: 100%; box-sizing: border-box; - grid-column: 1 / -1; /* 错误消息横跨所有列 */ + /* grid-column: 1 / -1; /* Let error messages flow naturally */ +} +.error-message.full-width-group { /* Ensure full-width error messages span */ + grid-column: 1 / -1; } .panel-footer { diff --git a/packages/frontend/src/locales/en.json b/packages/frontend/src/locales/en.json index 224ce23..c6a2f21 100644 --- a/packages/frontend/src/locales/en.json +++ b/packages/frontend/src/locales/en.json @@ -82,7 +82,11 @@ "editorFontSize": "Editor Font Size", "editorFontSizeSaved": "Editor font size saved.", "editorFontSizeSaveFailed": "Failed to save editor font size: {message}", - "errorInvalidEditorFontSize": "Invalid font size. Please enter a positive number." + "errorInvalidEditorFontSize": "Invalid font size. Please enter a positive number.", + "uiThemeJsonEditorTitle": "UI Theme JSON Editor", + "uiThemeJsonEditorDesc": "Directly edit the UI theme configuration using JSON. Changes here will reflect in the color pickers above after blurring the textarea.", + "errorInvalidJsonObject": "Invalid input. Please provide a valid JSON object.", + "errorInvalidJsonConfig": "Invalid JSON configuration" }, "login": { "title": "User Login", diff --git a/packages/frontend/src/locales/zh.json b/packages/frontend/src/locales/zh.json index f96d8c8..909ffe8 100644 --- a/packages/frontend/src/locales/zh.json +++ b/packages/frontend/src/locales/zh.json @@ -82,7 +82,11 @@ "editorFontSize": "编辑器字体大小", "editorFontSizeSaved": "编辑器字体大小已保存。", "editorFontSizeSaveFailed": "保存编辑器字体大小失败: {message}", - "errorInvalidEditorFontSize": "无效的字体大小。请输入一个正数。" + "errorInvalidEditorFontSize": "无效的字体大小。请输入一个正数。", + "uiThemeJsonEditorTitle": "界面主题 JSON 编辑器", + "uiThemeJsonEditorDesc": "直接使用 JSON 编辑界面主题配置。在此处更改并在文本区域失焦后,上面的颜色选择器将同步更新。", + "errorInvalidJsonObject": "输入无效。请输入一个有效的 JSON 对象。", + "errorInvalidJsonConfig": "无效的 JSON 配置" }, "login": { "title": "用户登录", diff --git a/packages/frontend/src/stores/default-themes.ts b/packages/frontend/src/stores/default-themes.ts index 0d16811..275f841 100644 --- a/packages/frontend/src/stores/default-themes.ts +++ b/packages/frontend/src/stores/default-themes.ts @@ -40,6 +40,13 @@ export const defaultUiTheme: Record = { '--button-bg-color': '#007bff', '--button-text-color': '#ffffff', '--button-hover-bg-color': '#0056b3', + // Added new variables + '--icon-color': '#666666', // Default to secondary text color + '--icon-hover-color': '#0056b3', // Default to link hover color + '--divider-color': '#cccccc', // Default to border color + '--input-focus-border-color': '#007bff', // Default to link active color + '--input-focus-glow-rgb': '0, 123, 255', // Default to link active color RGB + // End added variables '--font-family-sans-serif': 'sans-serif', '--base-padding': '1rem', '--base-margin': '0.5rem', diff --git a/packages/frontend/src/style.css b/packages/frontend/src/style.css index e15ddf2..6eb2275 100644 --- a/packages/frontend/src/style.css +++ b/packages/frontend/src/style.css @@ -15,6 +15,11 @@ --button-bg-color: #007bff; /* 按钮背景色 */ --button-text-color: #ffffff; /* 按钮文字颜色 */ --button-hover-bg-color: #0056b3;/* 按钮悬停背景色 */ + --icon-color: var(--text-color-secondary); /* 图标颜色 */ + --icon-hover-color: var(--link-hover-color); /* 图标悬停颜色 */ + --divider-color: var(--border-color); /* 分割线颜色 */ + --input-focus-border-color: var(--link-active-color); /* 输入框聚焦边框颜色 */ + --input-focus-glow-rgb: 0, 123, 255; /* 输入框聚焦光晕 RGB 值 (对应 --link-active-color) */ /* 字体 */ --font-family-sans-serif: sans-serif; /* 默认字体 */ @@ -44,6 +49,25 @@ a:hover { text-decoration: underline; /* 悬停时显示下划线 */ } +/* 全局图标样式 */ +i, .fas, .far, .fab { /* 根据你使用的图标库调整选择器 */ + color: var(--icon-color); + transition: color 0.2s ease; +} +a:hover i, a:hover .fas, a:hover .far, a:hover .fab, /* 链接内的图标 */ +button:hover i, button:hover .fas, button:hover .far, button:hover .fab, /* 按钮内的图标 */ +.icon-interactive:hover i, .icon-interactive:hover .fas, .icon-interactive:hover .far, .icon-interactive:hover .fab { /* 可交互图标容器 */ + color: var(--icon-hover-color); +} + +/* 全局分割线样式 */ +hr { + border: none; + border-top: 1px solid var(--divider-color); + margin: var(--base-margin) 0; +} + + /* 可以添加更多全局样式规则 */ /* 为 xterm 终端添加内边距 */ @@ -114,3 +138,10 @@ a:hover { ::-webkit-scrollbar-thumb:hover { background-color: var(--text-color-secondary); /* Scrollbar handle hover color */ } + +/* Input focus styles */ +input:focus, textarea:focus, select:focus { + border-color: var(--input-focus-border-color) !important; /* Use new variable, !important might be needed depending on specificity */ + outline: 0; + box-shadow: 0 0 0 3px rgba(var(--input-focus-glow-rgb), 0.2) !important; /* Use new variable, !important might be needed */ +} diff --git a/packages/frontend/src/views/SettingsView.vue b/packages/frontend/src/views/SettingsView.vue index 8d654ce..b90f08e 100644 --- a/packages/frontend/src/views/SettingsView.vue +++ b/packages/frontend/src/views/SettingsView.vue @@ -203,7 +203,7 @@