update
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user