update
This commit is contained in:
@@ -3,11 +3,14 @@ import { RouterLink, RouterView } from 'vue-router';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAuthStore } from './stores/auth.store';
|
import { useAuthStore } from './stores/auth.store';
|
||||||
import { useSettingsStore } from './stores/settings.store'; // 导入设置 Store
|
import { useSettingsStore } from './stores/settings.store'; // 导入设置 Store
|
||||||
|
import { ref } from 'vue'; // 导入 ref
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
// 导入通知显示组件
|
// 导入通知显示组件
|
||||||
import UINotificationDisplay from './components/UINotificationDisplay.vue';
|
import UINotificationDisplay from './components/UINotificationDisplay.vue';
|
||||||
// 导入文件编辑器弹窗组件
|
// 导入文件编辑器弹窗组件
|
||||||
import FileEditorOverlay from './components/FileEditorOverlay.vue';
|
import FileEditorOverlay from './components/FileEditorOverlay.vue';
|
||||||
|
// 导入样式自定义器组件
|
||||||
|
import StyleCustomizer from './components/StyleCustomizer.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -15,9 +18,22 @@ const settingsStore = useSettingsStore(); // 实例化设置 Store
|
|||||||
const { isAuthenticated } = storeToRefs(authStore); // 获取登录状态
|
const { isAuthenticated } = storeToRefs(authStore); // 获取登录状态
|
||||||
const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); // 获取弹窗编辑器设置
|
const { showPopupFileEditorBoolean } = storeToRefs(settingsStore); // 获取弹窗编辑器设置
|
||||||
|
|
||||||
|
// 控制样式自定义器可见性的状态
|
||||||
|
const isStyleCustomizerVisible = ref(false);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authStore.logout();
|
authStore.logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 打开样式自定义器
|
||||||
|
const openStyleCustomizer = () => {
|
||||||
|
isStyleCustomizerVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭样式自定义器 (由子组件触发)
|
||||||
|
const closeStyleCustomizer = () => {
|
||||||
|
isStyleCustomizerVisible.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -32,6 +48,7 @@ const handleLogout = () => {
|
|||||||
<RouterLink to="/notifications">{{ t('nav.notifications') }}</RouterLink> | <!-- 新增通知链接 -->
|
<RouterLink to="/notifications">{{ t('nav.notifications') }}</RouterLink> | <!-- 新增通知链接 -->
|
||||||
<RouterLink to="/audit-logs">{{ t('nav.auditLogs') }}</RouterLink> | <!-- 新增审计日志链接 -->
|
<RouterLink to="/audit-logs">{{ t('nav.auditLogs') }}</RouterLink> | <!-- 新增审计日志链接 -->
|
||||||
<RouterLink to="/settings">{{ t('nav.settings') }}</RouterLink> | <!-- 新增设置链接 -->
|
<RouterLink to="/settings">{{ t('nav.settings') }}</RouterLink> | <!-- 新增设置链接 -->
|
||||||
|
<a href="#" @click.prevent="openStyleCustomizer" :title="t('nav.customizeStyle')">🎨</a> | <!-- 添加调色板按钮 -->
|
||||||
<RouterLink v-if="!isAuthenticated" to="/login">{{ t('nav.login') }}</RouterLink>
|
<RouterLink v-if="!isAuthenticated" to="/login">{{ t('nav.login') }}</RouterLink>
|
||||||
<a href="#" v-if="isAuthenticated" @click.prevent="handleLogout">{{ t('nav.logout') }}</a>
|
<a href="#" v-if="isAuthenticated" @click.prevent="handleLogout">{{ t('nav.logout') }}</a>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -47,6 +64,9 @@ const handleLogout = () => {
|
|||||||
<!-- 根据设置条件渲染全局文件编辑器弹窗 -->
|
<!-- 根据设置条件渲染全局文件编辑器弹窗 -->
|
||||||
<FileEditorOverlay v-if="showPopupFileEditorBoolean" />
|
<FileEditorOverlay v-if="showPopupFileEditorBoolean" />
|
||||||
|
|
||||||
|
<!-- 条件渲染样式自定义器 -->
|
||||||
|
<StyleCustomizer v-if="isStyleCustomizerVisible" @close="closeStyleCustomizer" />
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<!-- 使用 t 函数获取应用名称 -->
|
<!-- 使用 t 函数获取应用名称 -->
|
||||||
<p>© 2025 {{ t('appName') }}</p>
|
<p>© 2025 {{ t('appName') }}</p>
|
||||||
@@ -59,38 +79,42 @@ const handleLogout = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
font-family: sans-serif;
|
font-family: var(--font-family-sans-serif); /* 使用字体变量 */
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background-color: #f0f0f0;
|
background-color: var(--header-bg-color); /* 使用头部背景色变量 */
|
||||||
padding: 1rem;
|
padding: var(--base-padding); /* 使用基础内边距变量 */
|
||||||
border-bottom: 1px solid #ccc;
|
border-bottom: 1px solid var(--border-color); /* 使用边框颜色变量 */
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a {
|
nav a {
|
||||||
margin: 0 0.5rem;
|
margin: 0 var(--base-margin); /* 使用基础外边距变量 */
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #333;
|
color: var(--link-color); /* 使用链接颜色变量 */
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover {
|
||||||
|
color: var(--link-hover-color); /* 使用链接悬停颜色变量 */
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a.router-link-exact-active {
|
nav a.router-link-exact-active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #007bff;
|
color: var(--link-active-color); /* 使用激活链接颜色变量 */
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 1rem;
|
padding: var(--base-padding); /* 使用基础内边距变量 */
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background-color: #f0f0f0;
|
background-color: var(--footer-bg-color); /* 使用底部背景色变量 */
|
||||||
padding: 0.5rem 1rem;
|
padding: calc(var(--base-padding) / 2) var(--base-padding); /* 调整内边距 */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #666;
|
color: var(--text-color-secondary); /* 使用次要文字颜色变量 */
|
||||||
border-top: 1px solid #ccc;
|
border-top: 1px solid var(--border-color); /* 使用边框颜色变量 */
|
||||||
margin-top: auto; /* Pushes footer to the bottom */
|
margin-top: auto; /* Pushes footer to the bottom */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -156,8 +156,8 @@ const handleSubmit = async () => {
|
|||||||
<small v-if="isEditMode">{{ t('proxies.form.passwordUpdateNote') }}</small>
|
<small v-if="isEditMode">{{ t('proxies.form.passwordUpdateNote') }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="formError || storeError" class="error-message">
|
<div v-if="formError || combinedStoreError" class="error-message">
|
||||||
{{ formError || storeError }}
|
{{ formError || combinedStoreError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import type { ITheme } from 'xterm'; // 导入 xterm 主题类型
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const { currentUiTheme, currentXtermTheme } = storeToRefs(settingsStore); // 获取响应式的主题状态
|
||||||
|
|
||||||
|
// 创建本地响应式副本用于编辑
|
||||||
|
const editableUiTheme = ref<Record<string, string>>({});
|
||||||
|
const editableXtermTheme = ref<ITheme>({});
|
||||||
|
|
||||||
|
// 初始化本地副本
|
||||||
|
const initializeEditableThemes = () => {
|
||||||
|
// 使用深拷贝确保不直接修改 store 状态
|
||||||
|
editableUiTheme.value = JSON.parse(JSON.stringify(currentUiTheme.value || {}));
|
||||||
|
editableXtermTheme.value = JSON.parse(JSON.stringify(currentXtermTheme.value || {}));
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(initializeEditableThemes);
|
||||||
|
|
||||||
|
// 如果 store 中的主题变化(例如通过重置),也更新本地副本
|
||||||
|
watch(currentUiTheme, initializeEditableThemes, { deep: true });
|
||||||
|
watch(currentXtermTheme, initializeEditableThemes, { deep: true });
|
||||||
|
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
|
const closeCustomizer = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 临时的编辑区域占位符
|
||||||
|
const currentTab = ref<'ui' | 'terminal'>('ui');
|
||||||
|
|
||||||
|
// --- 处理函数 ---
|
||||||
|
const handleSaveChanges = async () => {
|
||||||
|
try {
|
||||||
|
await settingsStore.saveCustomThemes(editableUiTheme.value, editableXtermTheme.value);
|
||||||
|
// 可以添加一个成功提示
|
||||||
|
closeCustomizer(); // 保存后关闭
|
||||||
|
} catch (error) {
|
||||||
|
console.error("保存主题失败:", error);
|
||||||
|
// 可以添加一个错误提示
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetDefault = async () => {
|
||||||
|
try {
|
||||||
|
await settingsStore.resetCustomThemes();
|
||||||
|
// 重置后本地副本会自动通过 watch 更新
|
||||||
|
// 可以添加一个成功提示
|
||||||
|
} catch (error) {
|
||||||
|
console.error("重置主题失败:", error);
|
||||||
|
// 可以添加一个错误提示
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:将 CSS 变量名转换为更友好的标签
|
||||||
|
const formatLabel = (key: string): string => {
|
||||||
|
// 简单的转换逻辑,可以根据需要优化
|
||||||
|
return key
|
||||||
|
.replace(/^--/, '') // 移除前缀 '--'
|
||||||
|
.replace(/-/g, ' ') // 替换 '-' 为空格
|
||||||
|
.replace(/([A-Z])/g, ' $1') // 在大写字母前加空格
|
||||||
|
.replace(/^./, (str) => str.toUpperCase()); // 首字母大写
|
||||||
|
};
|
||||||
|
|
||||||
|
// 辅助函数:将 xterm theme key 转换为更友好的标签
|
||||||
|
const formatXtermLabel = (key: keyof ITheme): string => {
|
||||||
|
// 简单的转换逻辑
|
||||||
|
return key.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="style-customizer-overlay" @click.self="closeCustomizer">
|
||||||
|
<div class="style-customizer-panel">
|
||||||
|
<header class="panel-header">
|
||||||
|
<h2>{{ t('styleCustomizer.title') }}</h2>
|
||||||
|
<button @click="closeCustomizer" class="close-button">×</button>
|
||||||
|
</header>
|
||||||
|
<div class="panel-content">
|
||||||
|
<nav class="panel-nav">
|
||||||
|
<button @click="currentTab = 'ui'" :class="{ active: currentTab === 'ui' }">
|
||||||
|
{{ t('styleCustomizer.uiStyles') }}
|
||||||
|
</button>
|
||||||
|
<button @click="currentTab = 'terminal'" :class="{ active: currentTab === 'terminal' }">
|
||||||
|
{{ t('styleCustomizer.terminalStyles') }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
<main class="panel-main">
|
||||||
|
<section v-if="currentTab === 'ui'">
|
||||||
|
<h3>{{ t('styleCustomizer.uiStyles') }}</h3>
|
||||||
|
<p>{{ t('styleCustomizer.uiDescription') }}</p>
|
||||||
|
<!-- 动态生成 UI 样式编辑控件 -->
|
||||||
|
<div v-for="(value, key) in editableUiTheme" :key="key" class="form-group">
|
||||||
|
<label :for="`ui-${key}`">{{ formatLabel(key) }}:</label>
|
||||||
|
<!-- 简单判断是否为颜色值,显示颜色选择器 -->
|
||||||
|
<input
|
||||||
|
v-if="typeof value === 'string' && (value.startsWith('#') || value.startsWith('rgb') || value.startsWith('hsl'))"
|
||||||
|
type="color"
|
||||||
|
:id="`ui-${key}`"
|
||||||
|
v-model="editableUiTheme[key]"
|
||||||
|
/>
|
||||||
|
<!-- 否则显示文本输入框 -->
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
:id="`ui-${key}`"
|
||||||
|
v-model="editableUiTheme[key]"
|
||||||
|
class="text-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section v-if="currentTab === 'terminal'">
|
||||||
|
<h3>{{ t('styleCustomizer.terminalStyles') }}</h3>
|
||||||
|
<p>{{ t('styleCustomizer.terminalDescription') }}</p>
|
||||||
|
<!-- 动态生成终端样式编辑控件 -->
|
||||||
|
<div v-for="(value, key) in editableXtermTheme" :key="key" class="form-group">
|
||||||
|
<label :for="`xterm-${key}`">{{ formatXtermLabel(key as keyof ITheme) }}:</label>
|
||||||
|
<!-- 简单判断是否为颜色值 -->
|
||||||
|
<input
|
||||||
|
v-if="typeof value === 'string' && value.startsWith('#')"
|
||||||
|
type="color"
|
||||||
|
:id="`xterm-${key}`"
|
||||||
|
v-model="(editableXtermTheme as any)[key]"
|
||||||
|
/>
|
||||||
|
<!-- 其他类型(如数字、布尔值)可以添加相应控件,这里简化为文本 -->
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
type="text"
|
||||||
|
:id="`xterm-${key}`"
|
||||||
|
v-model="(editableXtermTheme as any)[key]"
|
||||||
|
class="text-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<footer class="panel-footer">
|
||||||
|
<button @click="handleResetDefault" class="button-secondary">{{ t('styleCustomizer.resetDefault') }}</button>
|
||||||
|
<button @click="handleSaveChanges" class="button-primary">{{ t('styleCustomizer.saveChanges') }}</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.style-customizer-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000; /* 确保在顶层 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-customizer-panel {
|
||||||
|
background-color: var(--app-bg-color, #fff);
|
||||||
|
color: var(--text-color, #333);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
width: 80%;
|
||||||
|
max-width: 700px; /* 最大宽度 */
|
||||||
|
max-height: 80vh; /* 最大高度 */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden; /* 防止内容溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--base-padding, 1rem);
|
||||||
|
border-bottom: 1px solid var(--border-color, #ccc);
|
||||||
|
background-color: var(--header-bg-color, #f0f0f0); /* 使用头部背景色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color-secondary, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto; /* 内部滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-nav {
|
||||||
|
width: 150px; /* 固定导航宽度 */
|
||||||
|
border-right: 1px solid var(--border-color, #ccc);
|
||||||
|
padding: var(--base-padding, 1rem);
|
||||||
|
background-color: var(--header-bg-color, #f0f0f0); /* 轻微区分背景 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-nav button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color, #333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-nav button:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-nav button.active {
|
||||||
|
background-color: var(--link-active-color, #007bff);
|
||||||
|
color: var(--button-text-color, #fff);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-main {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: var(--base-padding, 1rem);
|
||||||
|
overflow-y: auto; /* 主要内容区域滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-main h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
border-bottom: 1px solid var(--border-color, #ccc);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-main p {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 150px; /* 调整标签最小宽度以适应更长的文本 */
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: right; /* 标签右对齐 */
|
||||||
|
padding-right: 5px; /* 标签和输入框间距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="color"] {
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 150px; /* 统一输入框宽度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"].text-input {
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 150px; /* 统一文本输入框宽度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: var(--base-padding, 1rem);
|
||||||
|
border-top: 1px solid var(--border-color, #ccc);
|
||||||
|
background-color: var(--footer-bg-color, #f0f0f0); /* 使用底部背景色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-footer button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-primary {
|
||||||
|
background-color: var(--button-bg-color, #007bff);
|
||||||
|
color: var(--button-text-color, #fff);
|
||||||
|
border-color: var(--button-bg-color, #007bff);
|
||||||
|
}
|
||||||
|
.button-primary:hover {
|
||||||
|
background-color: var(--button-hover-bg-color, #0056b3);
|
||||||
|
border-color: var(--button-hover-bg-color, #0056b3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-secondary {
|
||||||
|
background-color: #6c757d; /* 暂时硬编码,后续可改为变量 */
|
||||||
|
color: #fff;
|
||||||
|
border-color: #6c757d;
|
||||||
|
}
|
||||||
|
.button-secondary:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
border-color: #545b62;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'; // 重新导入 nextTick
|
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||||
import { ITheme } from 'xterm';
|
import { ITheme } from 'xterm';
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
|
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 store
|
||||||
|
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||||
import 'xterm/css/xterm.css'; // 引入 xterm 样式
|
import 'xterm/css/xterm.css'; // 引入 xterm 样式
|
||||||
@@ -27,6 +29,10 @@ let resizeObserver: ResizeObserver | null = null;
|
|||||||
let debounceTimer: number | null = null; // 用于防抖的计时器 ID
|
let debounceTimer: number | null = null; // 用于防抖的计时器 ID
|
||||||
const fontSize = ref(14); // 字体大小状态, 默认为14
|
const fontSize = ref(14); // 字体大小状态, 默认为14
|
||||||
|
|
||||||
|
// --- Settings Store ---
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const { currentXtermTheme } = storeToRefs(settingsStore); // 获取响应式的 xterm 主题
|
||||||
|
|
||||||
// 防抖函数
|
// 防抖函数
|
||||||
const debounce = (func: Function, delay: number) => {
|
const debounce = (func: Function, delay: number) => {
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
@@ -71,11 +77,7 @@ onMounted(() => {
|
|||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontSize: fontSize.value,
|
fontSize: fontSize.value,
|
||||||
fontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"',
|
fontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"',
|
||||||
theme: { // 简单主题示例
|
theme: currentXtermTheme.value, // *** 使用 store 中的当前 xterm 主题 ***
|
||||||
background: '#1e1e1e',
|
|
||||||
foreground: '#d4d4d4',
|
|
||||||
cursor: '#d4d4d4',
|
|
||||||
},
|
|
||||||
rows: 24, // 初始行数
|
rows: 24, // 初始行数
|
||||||
cols: 80, // 初始列数
|
cols: 80, // 初始列数
|
||||||
allowTransparency: true,
|
allowTransparency: true,
|
||||||
@@ -179,10 +181,20 @@ onMounted(() => {
|
|||||||
}, { immediate: true }); // 立即执行一次 watch
|
}, { immediate: true }); // 立即执行一次 watch
|
||||||
|
|
||||||
// 触发 ready 事件,传递 sessionId 和 terminal 实例
|
// 触发 ready 事件,传递 sessionId 和 terminal 实例
|
||||||
if (terminal) { // 确保 terminal 实例已创建
|
if (terminal) {
|
||||||
emit('ready', { sessionId: props.sessionId, terminal: terminal });
|
emit('ready', { sessionId: props.sessionId, terminal: terminal });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 监听 xterm 主题变化 ---
|
||||||
|
watch(currentXtermTheme, (newTheme) => {
|
||||||
|
if (terminal) {
|
||||||
|
console.log(`[Terminal ${props.sessionId}] Applying new xterm theme.`); // 日志改为中文
|
||||||
|
terminal.options.theme = newTheme;
|
||||||
|
// 可能需要重新渲染或刷新终端以完全应用主题,但通常 xterm 会自动处理
|
||||||
|
// terminal.refresh(0, terminal.rows - 1); // 如果需要强制刷新
|
||||||
|
}
|
||||||
|
}, { deep: true }); // 使用 deep watch
|
||||||
|
|
||||||
// 聚焦终端
|
// 聚焦终端
|
||||||
terminal.focus();
|
terminal.focus();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,17 @@
|
|||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"auditLogs": "Audit Logs",
|
"auditLogs": "Audit Logs",
|
||||||
"settings": "Settings"
|
"settings": "Settings",
|
||||||
|
"customizeStyle": "Customize Style"
|
||||||
|
},
|
||||||
|
"styleCustomizer": {
|
||||||
|
"title": "Style Customizer",
|
||||||
|
"uiStyles": "UI Styles",
|
||||||
|
"terminalStyles": "Terminal Styles",
|
||||||
|
"uiDescription": "Adjust colors, fonts, etc., for the application interface.",
|
||||||
|
"terminalDescription": "Customize the color scheme and font for the terminal.",
|
||||||
|
"resetDefault": "Reset Default",
|
||||||
|
"saveChanges": "Save Changes"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "User Login",
|
"title": "User Login",
|
||||||
|
|||||||
@@ -10,7 +10,17 @@
|
|||||||
"tags": "标签管理",
|
"tags": "标签管理",
|
||||||
"notifications": "通知管理",
|
"notifications": "通知管理",
|
||||||
"auditLogs": "审计日志",
|
"auditLogs": "审计日志",
|
||||||
"settings": "设置"
|
"settings": "设置",
|
||||||
|
"customizeStyle": "自定义样式"
|
||||||
|
},
|
||||||
|
"styleCustomizer": {
|
||||||
|
"title": "样式自定义",
|
||||||
|
"uiStyles": "界面样式",
|
||||||
|
"terminalStyles": "终端样式",
|
||||||
|
"uiDescription": "调整应用程序界面的颜色、字体等。",
|
||||||
|
"terminalDescription": "自定义终端的颜色方案和字体。",
|
||||||
|
"resetDefault": "恢复默认",
|
||||||
|
"saveChanges": "保存更改"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "用户登录",
|
"title": "用户登录",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ref, computed } from 'vue'; // Import computed
|
import { ref, computed, watch } from 'vue'; // Import computed and watch
|
||||||
import i18n, { setLocale, defaultLng } from '../i18n'; // Import i18n instance and setLocale
|
import i18n, { setLocale, defaultLng } from '../i18n'; // Import i18n instance and setLocale
|
||||||
|
import type { ITheme } from 'xterm'; // 导入 xterm 主题类型
|
||||||
|
|
||||||
// Define the type for settings state explicitly
|
// Define the type for settings state explicitly
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
@@ -10,16 +11,64 @@ interface SettingsState {
|
|||||||
maxLoginAttempts: string;
|
maxLoginAttempts: string;
|
||||||
loginBanDuration: string;
|
loginBanDuration: string;
|
||||||
showPopupFileEditor: string; // 弹窗编辑器设置
|
showPopupFileEditor: string; // 弹窗编辑器设置
|
||||||
shareFileEditorTabs?: string; // 新增:共享编辑器标签页设置 ('true'/'false')
|
shareFileEditorTabs?: string; // 共享编辑器标签页设置 ('true'/'false')
|
||||||
|
customUiTheme?: string; // UI 主题 (CSS 变量 JSON 字符串)
|
||||||
|
customXtermTheme?: string; // xterm 主题 (JSON 字符串)
|
||||||
// Add other settings keys here as needed
|
// Add other settings keys here as needed
|
||||||
[key: string]: string | undefined; // Allow other string settings, make value optional
|
[key: string]: string | undefined; // Allow other string settings, make value optional
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 默认 UI 主题 (CSS 变量)
|
||||||
|
const defaultUiTheme: Record<string, string> = {
|
||||||
|
'--app-bg-color': '#ffffff',
|
||||||
|
'--text-color': '#333333',
|
||||||
|
'--text-color-secondary': '#666666',
|
||||||
|
'--border-color': '#cccccc',
|
||||||
|
'--link-color': '#333',
|
||||||
|
'--link-hover-color': '#0056b3',
|
||||||
|
'--link-active-color': '#007bff',
|
||||||
|
'--header-bg-color': '#f0f0f0',
|
||||||
|
'--footer-bg-color': '#f0f0f0',
|
||||||
|
'--button-bg-color': '#007bff',
|
||||||
|
'--button-text-color': '#ffffff',
|
||||||
|
'--button-hover-bg-color': '#0056b3',
|
||||||
|
'--font-family-sans-serif': 'sans-serif',
|
||||||
|
'--base-padding': '1rem',
|
||||||
|
'--base-margin': '0.5rem',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 默认 xterm 主题
|
||||||
|
const defaultXtermTheme: ITheme = {
|
||||||
|
background: '#1e1e1e',
|
||||||
|
foreground: '#d4d4d4',
|
||||||
|
cursor: '#d4d4d4',
|
||||||
|
selectionBackground: '#264f78', // 使用 selectionBackground 而不是 selection
|
||||||
|
black: '#000000',
|
||||||
|
red: '#cd3131',
|
||||||
|
green: '#0dbc79',
|
||||||
|
yellow: '#e5e510',
|
||||||
|
blue: '#2472c8',
|
||||||
|
magenta: '#bc3fbc',
|
||||||
|
cyan: '#11a8cd',
|
||||||
|
white: '#e5e5e5',
|
||||||
|
brightBlack: '#666666',
|
||||||
|
brightRed: '#f14c4c',
|
||||||
|
brightGreen: '#23d18b',
|
||||||
|
brightYellow: '#f5f543',
|
||||||
|
brightBlue: '#3b8eea',
|
||||||
|
brightMagenta: '#d670d6',
|
||||||
|
brightCyan: '#29b8db',
|
||||||
|
brightWhite: '#e5e5e5'
|
||||||
|
};
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
// --- State ---
|
// --- State ---
|
||||||
const settings = ref<Partial<SettingsState>>({}); // Use Partial initially
|
const settings = ref<Partial<SettingsState>>({}); // Use Partial initially
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
const isStyleCustomizerVisible = ref(false); // 控制样式编辑器可见性
|
||||||
|
const currentUiTheme = ref<Record<string, string>>({ ...defaultUiTheme }); // 当前应用的 UI 主题
|
||||||
|
const currentXtermTheme = ref<ITheme>({ ...defaultXtermTheme }); // 当前应用的 xterm 主题
|
||||||
|
|
||||||
// --- Actions ---
|
// --- Actions ---
|
||||||
|
|
||||||
@@ -39,21 +88,24 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
const response = await axios.get<Record<string, string>>('/api/v1/settings');
|
const response = await axios.get<Record<string, string>>('/api/v1/settings');
|
||||||
settings.value = response.data; // Store all fetched settings
|
settings.value = response.data; // Store all fetched settings
|
||||||
console.log('[SettingsStore] Fetched settings raw:', JSON.stringify(response.data)); // 打印原始响应
|
console.log('[SettingsStore] Fetched settings raw:', JSON.stringify(response.data)); // 打印原始响应
|
||||||
console.log('[SettingsStore] Raw showPopupFileEditor from backend:', response.data.showPopupFileEditor); // <--- 添加日志:打印原始值
|
console.log('[SettingsStore] Raw showPopupFileEditor from backend:', response.data.showPopupFileEditor);
|
||||||
|
|
||||||
// --- 设置默认值 (如果后端未返回) ---
|
// --- 设置默认值 (如果后端未返回) ---
|
||||||
// 弹窗编辑器设置
|
// 弹窗编辑器设置 (保持不变)
|
||||||
if (settings.value.showPopupFileEditor === undefined) {
|
if (settings.value.showPopupFileEditor === undefined) {
|
||||||
console.log('[SettingsStore] showPopupFileEditor is undefined, setting default: true'); // 修改日志
|
console.log('[SettingsStore] showPopupFileEditor is undefined, setting default: true');
|
||||||
settings.value.showPopupFileEditor = 'true'; // 默认为 true
|
settings.value.showPopupFileEditor = 'true';
|
||||||
}
|
}
|
||||||
// 共享编辑器标签页设置
|
// 共享编辑器标签页设置 (保持不变)
|
||||||
if (settings.value.shareFileEditorTabs === undefined) {
|
if (settings.value.shareFileEditorTabs === undefined) {
|
||||||
console.log('[SettingsStore] Setting default for shareFileEditorTabs: true');
|
console.log('[SettingsStore] Setting default for shareFileEditorTabs: true');
|
||||||
settings.value.shareFileEditorTabs = 'true'; // 默认为 true (共享)
|
settings.value.shareFileEditorTabs = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 语言设置 ---
|
// --- 加载自定义主题 ---
|
||||||
|
loadAndApplyThemesFromSettings(); // 新增:加载并应用主题
|
||||||
|
|
||||||
|
// --- 语言设置 (保持不变) ---
|
||||||
// Determine and apply language
|
// Determine and apply language
|
||||||
const langFromSettings = settings.value.language;
|
const langFromSettings = settings.value.language;
|
||||||
if (langFromSettings === 'en' || langFromSettings === 'zh') {
|
if (langFromSettings === 'en' || langFromSettings === 'zh') {
|
||||||
@@ -67,17 +119,15 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
// await updateSetting('language', fetchedLang);
|
// await updateSetting('language', fetchedLang);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure fetchedLang is valid before calling setLocale
|
// Ensure fetchedLang is valid before calling setLocale (保持不变)
|
||||||
if (fetchedLang) {
|
if (fetchedLang) {
|
||||||
console.log(`[SettingsStore] Determined language: ${fetchedLang}. Applying locale...`); // 添加日志
|
console.log(`[SettingsStore] Determined language: ${fetchedLang}. Applying locale...`);
|
||||||
setLocale(fetchedLang); // Apply the determined locale
|
setLocale(fetchedLang);
|
||||||
} else {
|
} else {
|
||||||
// This case should ideally not happen due to fallback logic, but as a safeguard:
|
|
||||||
console.error('[SettingsStore] Could not determine a valid language to set.');
|
console.error('[SettingsStore] Could not determine a valid language to set.');
|
||||||
setLocale(defaultLng); // Fallback to default if determination failed
|
setLocale(defaultLng);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to load initial settings:', err);
|
console.error('Failed to load initial settings:', err);
|
||||||
error.value = err.response?.data?.message || err.message || 'Failed to load settings';
|
error.value = err.response?.data?.message || err.message || 'Failed to load settings';
|
||||||
@@ -88,26 +138,80 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 settings ref 加载主题设置,解析并应用它们。
|
||||||
|
*/
|
||||||
|
function loadAndApplyThemesFromSettings() {
|
||||||
|
// 加载 UI 主题
|
||||||
|
try {
|
||||||
|
if (settings.value.customUiTheme) {
|
||||||
|
const parsedUiTheme = JSON.parse(settings.value.customUiTheme);
|
||||||
|
// 合并默认值,确保所有变量都存在
|
||||||
|
currentUiTheme.value = { ...defaultUiTheme, ...parsedUiTheme };
|
||||||
|
} else {
|
||||||
|
currentUiTheme.value = { ...defaultUiTheme }; // 使用默认值
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SettingsStore] Failed to parse custom UI theme, using default:', e);
|
||||||
|
currentUiTheme.value = { ...defaultUiTheme };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载 xterm 主题
|
||||||
|
try {
|
||||||
|
if (settings.value.customXtermTheme) {
|
||||||
|
const parsedXtermTheme = JSON.parse(settings.value.customXtermTheme);
|
||||||
|
// 合并默认值
|
||||||
|
currentXtermTheme.value = { ...defaultXtermTheme, ...parsedXtermTheme };
|
||||||
|
} else {
|
||||||
|
currentXtermTheme.value = { ...defaultXtermTheme }; // 使用默认值
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[SettingsStore] Failed to parse custom xterm theme, using default:', e);
|
||||||
|
currentXtermTheme.value = { ...defaultXtermTheme };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用加载的主题
|
||||||
|
applyUiTheme(currentUiTheme.value);
|
||||||
|
// xterm 主题的应用将在 Terminal 组件内部通过 watch 监听 currentXtermTheme 实现
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 UI 主题 (CSS 变量) 应用到文档根元素。
|
||||||
|
* @param theme 要应用的 UI 主题对象。
|
||||||
|
*/
|
||||||
|
function applyUiTheme(theme: Record<string, string>) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
for (const [key, value] of Object.entries(theme)) {
|
||||||
|
root.style.setProperty(key, value);
|
||||||
|
}
|
||||||
|
console.log('[SettingsStore] Applied UI theme:', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 currentUiTheme 的变化并自动应用
|
||||||
|
watch(currentUiTheme, (newTheme) => {
|
||||||
|
applyUiTheme(newTheme);
|
||||||
|
}, { deep: true }); // 使用 deep watch 监听对象内部变化
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a single setting value both locally and on the backend.
|
* Updates a single setting value both locally and on the backend.
|
||||||
* @param key The setting key to update.
|
* @param key The setting key to update.
|
||||||
* @param value The new value for the setting.
|
* @param value The new value for the setting.
|
||||||
*/
|
*/
|
||||||
async function updateSetting(key: keyof SettingsState, value: string) {
|
async function updateSetting(key: keyof SettingsState, value: string) {
|
||||||
// const previousValue = settings.value[key]; // No longer needed for optimistic revert
|
|
||||||
// settings.value = { ...settings.value, [key]: value }; // Remove optimistic update
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.put('/api/v1/settings', { [key]: value });
|
await axios.put('/api/v1/settings', { [key]: value });
|
||||||
// Update store state *after* successful API call
|
// Update store state *after* successful API call
|
||||||
settings.value = { ...settings.value, [key]: value };
|
settings.value = { ...settings.value, [key]: value };
|
||||||
|
// 如果更新的是主题设置,需要重新解析和应用
|
||||||
|
if (key === 'customUiTheme' || key === 'customXtermTheme') {
|
||||||
|
loadAndApplyThemesFromSettings();
|
||||||
|
}
|
||||||
// If updating language, also update i18n
|
// If updating language, also update i18n
|
||||||
if (key === 'language' && (value === 'en' || value === 'zh')) {
|
if (key === 'language' && (value === 'en' || value === 'zh')) {
|
||||||
setLocale(value);
|
setLocale(value);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(`Failed to update setting '${key}':`, err);
|
console.error(`Failed to update setting '${key}':`, err);
|
||||||
// settings.value = { ...settings.value, [key]: previousValue }; // Remove revert logic
|
|
||||||
throw new Error(err.response?.data?.message || err.message || `Failed to update setting '${key}'`);
|
throw new Error(err.response?.data?.message || err.message || `Failed to update setting '${key}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,51 +221,85 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
* @param updates An object containing key-value pairs of settings to update.
|
* @param updates An object containing key-value pairs of settings to update.
|
||||||
*/
|
*/
|
||||||
async function updateMultipleSettings(updates: Partial<SettingsState>) {
|
async function updateMultipleSettings(updates: Partial<SettingsState>) {
|
||||||
// const previousSettings = { ...settings.value }; // No longer needed for optimistic revert
|
|
||||||
// settings.value = { ...settings.value, ...updates }; // Remove optimistic update
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.put('/api/v1/settings', updates);
|
await axios.put('/api/v1/settings', updates);
|
||||||
// Update store state *after* successful API call
|
// Update store state *after* successful API call
|
||||||
settings.value = { ...settings.value, ...updates };
|
settings.value = { ...settings.value, ...updates };
|
||||||
|
// 如果更新包含主题设置,需要重新解析和应用
|
||||||
|
if (updates.customUiTheme !== undefined || updates.customXtermTheme !== undefined) {
|
||||||
|
loadAndApplyThemesFromSettings();
|
||||||
|
}
|
||||||
// If language is updated, apply it
|
// If language is updated, apply it
|
||||||
if (updates.language && (updates.language === 'en' || updates.language === 'zh')) {
|
if (updates.language && (updates.language === 'en' || updates.language === 'zh')) {
|
||||||
setLocale(updates.language);
|
setLocale(updates.language);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to update multiple settings:', err);
|
console.error('Failed to update multiple settings:', err);
|
||||||
// settings.value = previousSettings; // Remove revert logic
|
|
||||||
throw new Error(err.response?.data?.message || err.message || 'Failed to update settings');
|
throw new Error(err.response?.data?.message || err.message || 'Failed to update settings');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前编辑器中的自定义主题到后端。
|
||||||
|
* @param uiTheme UI 主题对象
|
||||||
|
* @param xtermTheme xterm 主题对象
|
||||||
|
*/
|
||||||
|
async function saveCustomThemes(uiTheme: Record<string, string>, xtermTheme: ITheme) {
|
||||||
|
const updates: Partial<SettingsState> = {
|
||||||
|
customUiTheme: JSON.stringify(uiTheme),
|
||||||
|
customXtermTheme: JSON.stringify(xtermTheme),
|
||||||
|
};
|
||||||
|
// 更新本地状态以立即反映(虽然 watch 也会触发应用,但这里更新 state 是必要的)
|
||||||
|
currentUiTheme.value = { ...uiTheme };
|
||||||
|
currentXtermTheme.value = { ...xtermTheme };
|
||||||
|
// 调用 updateMultipleSettings 保存到后端
|
||||||
|
await updateMultipleSettings(updates);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Getters ---
|
/**
|
||||||
|
* 重置为默认主题并保存。
|
||||||
|
*/
|
||||||
|
async function resetCustomThemes() {
|
||||||
|
await saveCustomThemes(defaultUiTheme, defaultXtermTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换样式编辑器面板的可见性。
|
||||||
|
* @param visible 可选,强制设置可见性
|
||||||
|
*/
|
||||||
|
function toggleStyleCustomizer(visible?: boolean) {
|
||||||
|
isStyleCustomizerVisible.value = visible === undefined ? !isStyleCustomizerVisible.value : visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Getters --- (保持不变)
|
||||||
// --- Getters ---
|
// --- Getters ---
|
||||||
const language = computed(() => settings.value.language || defaultLng);
|
const language = computed(() => settings.value.language || defaultLng);
|
||||||
|
|
||||||
// Getter for the popup editor setting, returning boolean
|
// Getter for the popup editor setting, returning boolean (保持不变)
|
||||||
const showPopupFileEditorBoolean = computed(() => {
|
const showPopupFileEditorBoolean = computed(() => {
|
||||||
// 默认为 true,除非明确设置为 'false'
|
return settings.value.showPopupFileEditor !== 'false';
|
||||||
return settings.value.showPopupFileEditor !== 'false'; // <-- 修正:检查正确的键 showPopupFileEditor
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Getter for sharing setting, returning boolean
|
// Getter for sharing setting, returning boolean (保持不变)
|
||||||
const shareFileEditorTabsBoolean = computed(() => {
|
const shareFileEditorTabsBoolean = computed(() => {
|
||||||
// 默认为 true (共享),除非明确设置为 'false'
|
|
||||||
return settings.value.shareFileEditorTabs !== 'false';
|
return settings.value.shareFileEditorTabs !== 'false';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings, // 原始设置对象 (可能包含字符串化的主题)
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
language, // Expose language getter
|
language,
|
||||||
showPopupFileEditorBoolean, // Expose boolean getter for popup editor setting
|
showPopupFileEditorBoolean,
|
||||||
shareFileEditorTabsBoolean: shareFileEditorTabsBoolean, // Expose boolean getter for sharing setting
|
shareFileEditorTabsBoolean,
|
||||||
|
isStyleCustomizerVisible, // 暴露编辑器可见状态
|
||||||
|
currentUiTheme, // 暴露当前应用的 UI 主题对象
|
||||||
|
currentXtermTheme, // 暴露当前应用的 xterm 主题对象
|
||||||
loadInitialSettings,
|
loadInitialSettings,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
updateMultipleSettings,
|
updateMultipleSettings,
|
||||||
|
saveCustomThemes, // 暴露保存主题 action
|
||||||
|
resetCustomThemes, // 暴露重置主题 action
|
||||||
|
toggleStyleCustomizer, // 暴露切换编辑器 action
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1 +1,47 @@
|
|||||||
/* Global styles will go here */
|
/* 全局样式和 CSS 变量定义 */
|
||||||
|
:root {
|
||||||
|
/* 基础颜色 */
|
||||||
|
--app-bg-color: #ffffff; /* 应用背景色 */
|
||||||
|
--text-color: #333333; /* 主要文字颜色 */
|
||||||
|
--text-color-secondary: #666666; /* 次要文字颜色 */
|
||||||
|
--border-color: #cccccc; /* 边框颜色 */
|
||||||
|
--link-color: #333; /* 链接颜色 */
|
||||||
|
--link-hover-color: #0056b3; /* 链接悬停颜色 */
|
||||||
|
--link-active-color: #007bff; /* 激活链接/主题色 */
|
||||||
|
|
||||||
|
/* 组件颜色 */
|
||||||
|
--header-bg-color: #f0f0f0; /* 头部背景色 */
|
||||||
|
--footer-bg-color: #f0f0f0; /* 底部背景色 */
|
||||||
|
--button-bg-color: #007bff; /* 按钮背景色 */
|
||||||
|
--button-text-color: #ffffff; /* 按钮文字颜色 */
|
||||||
|
--button-hover-bg-color: #0056b3;/* 按钮悬停背景色 */
|
||||||
|
|
||||||
|
/* 字体 */
|
||||||
|
--font-family-sans-serif: sans-serif; /* 默认字体 */
|
||||||
|
|
||||||
|
/* 其他 */
|
||||||
|
--base-padding: 1rem; /* 基础内边距 */
|
||||||
|
--base-margin: 0.5rem; /* 基础外边距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 应用基础样式 */
|
||||||
|
body {
|
||||||
|
margin: 0; /* 移除默认 body margin */
|
||||||
|
font-family: var(--font-family-sans-serif);
|
||||||
|
background-color: var(--app-bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.6; /* 改善可读性 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局链接样式 */
|
||||||
|
a {
|
||||||
|
color: var(--link-color);
|
||||||
|
text-decoration: none; /* 移除下划线 */
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--link-hover-color);
|
||||||
|
text-decoration: underline; /* 悬停时显示下划线 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 可以添加更多全局样式规则 */
|
||||||
|
|||||||
Reference in New Issue
Block a user