This commit is contained in:
Baobhan Sith
2025-04-19 10:20:55 +08:00
parent 49a4b34eb3
commit 62e39dc75c
6 changed files with 247 additions and 4 deletions
@@ -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<string | null>(null); // 用于显示 JSON 解析错误
// 终端主题管理相关状态
const selectedTerminalThemeId = ref<string | null>(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"
/>
</div>
<!-- UI Theme Textarea -->
<hr style="margin-top: calc(var(--base-padding) * 2); margin-bottom: calc(var(--base-padding) * 2);">
<h4>{{ t('styleCustomizer.uiThemeJsonEditorTitle') }}</h4> <!-- TODO: Add translation -->
<p>{{ t('styleCustomizer.uiThemeJsonEditorDesc') }}</p> <!-- TODO: Add translation -->
<div class="form-group full-width-group"> <!-- Use a class for full width -->
<label for="uiThemeTextarea" class="sr-only">{{ t('styleCustomizer.uiThemeJsonEditorTitle') }}</label> <!-- Screen reader only label -->
<textarea
id="uiThemeTextarea"
v-model="editableUiThemeString"
@blur="handleUiThemeStringChange"
rows="15"
:placeholder="'--app-bg-color: #ffffff\n--text-color: #333333\n...'"
spellcheck="false"
class="json-textarea"
></textarea>
</div>
<p v-if="themeParseError" class="error-message full-width-group">{{ themeParseError }}</p>
</section>
<section v-if="currentTab === 'terminal' && !isEditingTheme">
<h3>{{ t('styleCustomizer.terminalStyles') }}</h3>
@@ -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 {
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -82,7 +82,11 @@
"editorFontSize": "编辑器字体大小",
"editorFontSizeSaved": "编辑器字体大小已保存。",
"editorFontSizeSaveFailed": "保存编辑器字体大小失败: {message}",
"errorInvalidEditorFontSize": "无效的字体大小。请输入一个正数。"
"errorInvalidEditorFontSize": "无效的字体大小。请输入一个正数。",
"uiThemeJsonEditorTitle": "界面主题 JSON 编辑器",
"uiThemeJsonEditorDesc": "直接使用 JSON 编辑界面主题配置。在此处更改并在文本区域失焦后,上面的颜色选择器将同步更新。",
"errorInvalidJsonObject": "输入无效。请输入一个有效的 JSON 对象。",
"errorInvalidJsonConfig": "无效的 JSON 配置"
},
"login": {
"title": "用户登录",
@@ -40,6 +40,13 @@ export const defaultUiTheme: Record<string, string> = {
'--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',
+31
View File
@@ -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 */
}
+1 -1
View File
@@ -203,7 +203,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed, reactive, watch } from 'vue'; // 移除 toRefs
import { ref, onMounted, computed, reactive, watch } from 'vue';
import { useAuthStore } from '../stores/auth.store';
import { useSettingsStore } from '../stores/settings.store';
import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 store