update
This commit is contained in:
@@ -30,6 +30,8 @@ const editableTerminalFontSize = ref(14);
|
|||||||
const editableEditorFontSize = ref(14); // <-- 新增,编辑器字体大小
|
const editableEditorFontSize = ref(14); // <-- 新增,编辑器字体大小
|
||||||
// const editablePageBackgroundOpacity = ref(1.0); // Removed
|
// const editablePageBackgroundOpacity = ref(1.0); // Removed
|
||||||
// const editableTerminalBackgroundOpacity = 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
|
const selectedTerminalThemeId = ref<string | null>(null); // 下拉框选择的 ID
|
||||||
@@ -63,6 +65,21 @@ const initializeEditableState = () => {
|
|||||||
uploadError.value = null;
|
uploadError.value = null;
|
||||||
importError.value = null;
|
importError.value = null;
|
||||||
saveThemeError.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);
|
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 () => {
|
const handleSaveTerminalFont = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -436,6 +579,23 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
|||||||
class="text-input"
|
class="text-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
<section v-if="currentTab === 'terminal' && !isEditingTheme">
|
<section v-if="currentTab === 'terminal' && !isEditingTheme">
|
||||||
<h3>{{ t('styleCustomizer.terminalStyles') }}</h3>
|
<h3>{{ t('styleCustomizer.terminalStyles') }}</h3>
|
||||||
@@ -721,6 +881,19 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
|||||||
align-items: center; /* Vertically center items in the row */
|
align-items: center; /* Vertically center items in the row */
|
||||||
gap: calc(var(--base-margin) * 1.5); /* Increase gap slightly for better spacing */
|
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) */
|
/* Adjust grid for rows without a third element (like inline buttons) */
|
||||||
.form-group > *:nth-child(2):last-child {
|
.form-group > *:nth-child(2):last-child {
|
||||||
grid-column: 2 / 4; /* Let the second element span if it's the last */
|
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;
|
outline: 0;
|
||||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15); /* 调整聚焦阴影 */
|
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 */
|
/* Inline buttons within form-group */
|
||||||
@@ -1070,7 +1264,10 @@ hr {
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
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 {
|
.panel-footer {
|
||||||
|
|||||||
@@ -82,7 +82,11 @@
|
|||||||
"editorFontSize": "Editor Font Size",
|
"editorFontSize": "Editor Font Size",
|
||||||
"editorFontSizeSaved": "Editor font size saved.",
|
"editorFontSizeSaved": "Editor font size saved.",
|
||||||
"editorFontSizeSaveFailed": "Failed to save editor font size: {message}",
|
"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": {
|
"login": {
|
||||||
"title": "User Login",
|
"title": "User Login",
|
||||||
|
|||||||
@@ -82,7 +82,11 @@
|
|||||||
"editorFontSize": "编辑器字体大小",
|
"editorFontSize": "编辑器字体大小",
|
||||||
"editorFontSizeSaved": "编辑器字体大小已保存。",
|
"editorFontSizeSaved": "编辑器字体大小已保存。",
|
||||||
"editorFontSizeSaveFailed": "保存编辑器字体大小失败: {message}",
|
"editorFontSizeSaveFailed": "保存编辑器字体大小失败: {message}",
|
||||||
"errorInvalidEditorFontSize": "无效的字体大小。请输入一个正数。"
|
"errorInvalidEditorFontSize": "无效的字体大小。请输入一个正数。",
|
||||||
|
"uiThemeJsonEditorTitle": "界面主题 JSON 编辑器",
|
||||||
|
"uiThemeJsonEditorDesc": "直接使用 JSON 编辑界面主题配置。在此处更改并在文本区域失焦后,上面的颜色选择器将同步更新。",
|
||||||
|
"errorInvalidJsonObject": "输入无效。请输入一个有效的 JSON 对象。",
|
||||||
|
"errorInvalidJsonConfig": "无效的 JSON 配置"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "用户登录",
|
"title": "用户登录",
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ export const defaultUiTheme: Record<string, string> = {
|
|||||||
'--button-bg-color': '#007bff',
|
'--button-bg-color': '#007bff',
|
||||||
'--button-text-color': '#ffffff',
|
'--button-text-color': '#ffffff',
|
||||||
'--button-hover-bg-color': '#0056b3',
|
'--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',
|
'--font-family-sans-serif': 'sans-serif',
|
||||||
'--base-padding': '1rem',
|
'--base-padding': '1rem',
|
||||||
'--base-margin': '0.5rem',
|
'--base-margin': '0.5rem',
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
--button-bg-color: #007bff; /* 按钮背景色 */
|
--button-bg-color: #007bff; /* 按钮背景色 */
|
||||||
--button-text-color: #ffffff; /* 按钮文字颜色 */
|
--button-text-color: #ffffff; /* 按钮文字颜色 */
|
||||||
--button-hover-bg-color: #0056b3;/* 按钮悬停背景色 */
|
--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; /* 默认字体 */
|
--font-family-sans-serif: sans-serif; /* 默认字体 */
|
||||||
@@ -44,6 +49,25 @@ a:hover {
|
|||||||
text-decoration: underline; /* 悬停时显示下划线 */
|
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 终端添加内边距 */
|
/* 为 xterm 终端添加内边距 */
|
||||||
@@ -114,3 +138,10 @@ a:hover {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: var(--text-color-secondary); /* Scrollbar handle hover color */
|
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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { useAuthStore } from '../stores/auth.store';
|
||||||
import { useSettingsStore } from '../stores/settings.store';
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 store
|
import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 store
|
||||||
|
|||||||
Reference in New Issue
Block a user