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 {