update
This commit is contained in:
@@ -1,65 +1,356 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { ref, reactive, onMounted, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useSettingsStore } from '../stores/settings.store';
|
||||
import { useAppearanceStore } from '../stores/appearance.store'; // 使用新的 store
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ITheme } from 'xterm'; // 导入 xterm 主题类型
|
||||
import type { ITheme } from 'xterm';
|
||||
import type { TerminalTheme } from '../../../backend/src/types/terminal-theme.types'; // 引入类型
|
||||
import { defaultXtermTheme } from '../stores/default-themes'; // 引入默认主题
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { currentUiTheme, currentXtermTheme } = storeToRefs(settingsStore); // 获取响应式的主题状态
|
||||
const appearanceStore = useAppearanceStore();
|
||||
const {
|
||||
currentUiTheme,
|
||||
// currentTerminalTheme, // 这个是计算属性,只读,在编辑时不需要直接用
|
||||
activeTerminalThemeId,
|
||||
availableTerminalThemes,
|
||||
currentTerminalFontFamily,
|
||||
pageBackgroundImage,
|
||||
pageBackgroundOpacity,
|
||||
terminalBackgroundImage,
|
||||
terminalBackgroundOpacity,
|
||||
} = storeToRefs(appearanceStore);
|
||||
|
||||
// 创建本地响应式副本用于编辑
|
||||
// --- 本地状态用于编辑 ---
|
||||
const editableUiTheme = ref<Record<string, string>>({});
|
||||
const editableXtermTheme = ref<ITheme>({});
|
||||
const editableTerminalFontFamily = ref('');
|
||||
const editablePageBackgroundOpacity = ref(1.0);
|
||||
const editableTerminalBackgroundOpacity = ref(1.0);
|
||||
|
||||
// 初始化本地副本
|
||||
const initializeEditableThemes = () => {
|
||||
// 使用深拷贝确保不直接修改 store 状态
|
||||
// 终端主题管理相关状态
|
||||
const selectedTerminalThemeId = ref<string | null>(null); // 下拉框选择的 ID
|
||||
const isEditingTheme = ref(false); // 是否正在编辑某个主题
|
||||
// 使用 reactive 确保嵌套对象 themeData 的响应性
|
||||
// 修正:editingTheme 应该是一个 ref 包含 TerminalTheme 或 null
|
||||
const editingTheme = ref<TerminalTheme | null>(null); // 正在编辑的主题数据副本 (完整结构)
|
||||
const newThemeName = ref(''); // 新建主题的名称 (不再需要,直接编辑 editingTheme.value.name)
|
||||
|
||||
// 文件上传相关
|
||||
const pageBgFileInput = ref<HTMLInputElement | null>(null);
|
||||
const terminalBgFileInput = ref<HTMLInputElement | null>(null);
|
||||
const themeImportInput = ref<HTMLInputElement | null>(null);
|
||||
const uploadError = ref<string | null>(null);
|
||||
const importError = ref<string | null>(null);
|
||||
const saveThemeError = ref<string | null>(null); // 用于显示保存主题时的错误
|
||||
|
||||
|
||||
// 初始化本地编辑状态
|
||||
const initializeEditableState = () => {
|
||||
// 深拷贝 UI 主题
|
||||
editableUiTheme.value = JSON.parse(JSON.stringify(currentUiTheme.value || {}));
|
||||
editableXtermTheme.value = JSON.parse(JSON.stringify(currentXtermTheme.value || {}));
|
||||
editableTerminalFontFamily.value = currentTerminalFontFamily.value;
|
||||
selectedTerminalThemeId.value = activeTerminalThemeId.value ?? null; // 初始化下拉框
|
||||
editablePageBackgroundOpacity.value = pageBackgroundOpacity.value;
|
||||
editableTerminalBackgroundOpacity.value = terminalBackgroundOpacity.value;
|
||||
// 不在 store 变化时重置编辑状态,除非是显式取消或保存
|
||||
uploadError.value = null;
|
||||
importError.value = null;
|
||||
saveThemeError.value = null;
|
||||
};
|
||||
|
||||
onMounted(initializeEditableThemes);
|
||||
onMounted(initializeEditableState);
|
||||
|
||||
// 如果 store 中的主题变化(例如通过重置),也更新本地副本
|
||||
watch(currentUiTheme, initializeEditableThemes, { deep: true });
|
||||
watch(currentXtermTheme, initializeEditableThemes, { deep: true });
|
||||
// 监听 store 变化以更新本地状态 (例如重置或外部更改)
|
||||
// 只监听不需要编辑的状态或用于初始化的状态
|
||||
watch([
|
||||
currentUiTheme, currentTerminalFontFamily, activeTerminalThemeId,
|
||||
pageBackgroundOpacity, terminalBackgroundOpacity
|
||||
], (newVals, oldVals) => {
|
||||
// 仅当非编辑状态时,或活动主题ID变化时,才同步下拉框和非编辑状态
|
||||
if (!isEditingTheme.value || newVals[2] !== oldVals[2]) {
|
||||
initializeEditableState();
|
||||
} else {
|
||||
// 如果正在编辑,只更新非编辑相关的部分 (例如 UI 主题可以在编辑终端主题时同时更新)
|
||||
editableUiTheme.value = JSON.parse(JSON.stringify(newVals[0] || {}));
|
||||
editableTerminalFontFamily.value = newVals[1];
|
||||
editablePageBackgroundOpacity.value = newVals[3];
|
||||
editableTerminalBackgroundOpacity.value = newVals[4];
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const closeCustomizer = () => {
|
||||
emit('close');
|
||||
// 如果正在编辑主题,提示用户是否放弃更改
|
||||
if (isEditingTheme.value) {
|
||||
if (confirm(t('styleCustomizer.confirmCloseEditing'))) {
|
||||
isEditingTheme.value = false; // 退出编辑状态
|
||||
editingTheme.value = null;
|
||||
emit('close');
|
||||
}
|
||||
} else {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
// 临时的编辑区域占位符
|
||||
const currentTab = ref<'ui' | 'terminal'>('ui');
|
||||
// 当前活动的标签页
|
||||
const currentTab = ref<'ui' | 'terminal' | 'background'>('ui');
|
||||
|
||||
// --- 处理函数 ---
|
||||
const handleSaveChanges = async () => {
|
||||
|
||||
// 保存 UI 主题更改
|
||||
const handleSaveUiTheme = async () => {
|
||||
try {
|
||||
await settingsStore.saveCustomThemes(editableUiTheme.value, editableXtermTheme.value);
|
||||
// 可以添加一个成功提示
|
||||
closeCustomizer(); // 保存后关闭
|
||||
} catch (error) {
|
||||
console.error("保存主题失败:", error);
|
||||
// 可以添加一个错误提示
|
||||
await appearanceStore.saveCustomUiTheme(editableUiTheme.value);
|
||||
alert(t('styleCustomizer.uiThemeSaved')); // 简单提示
|
||||
} catch (error: any) {
|
||||
console.error("保存 UI 主题失败:", error);
|
||||
alert(t('styleCustomizer.uiThemeSaveFailed', { message: error.message }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetDefault = async () => {
|
||||
try {
|
||||
await settingsStore.resetCustomThemes();
|
||||
// 重置后本地副本会自动通过 watch 更新
|
||||
// 可以添加一个成功提示
|
||||
} catch (error) {
|
||||
console.error("重置主题失败:", error);
|
||||
// 可以添加一个错误提示
|
||||
}
|
||||
// 重置 UI 主题
|
||||
const handleResetUiTheme = async () => {
|
||||
if (confirm(t('styleCustomizer.confirmResetUi'))) {
|
||||
try {
|
||||
await appearanceStore.resetCustomUiTheme();
|
||||
// watch 会自动更新 editableUiTheme.value
|
||||
alert(t('styleCustomizer.uiThemeReset'));
|
||||
} catch (error: any) {
|
||||
console.error("重置 UI 主题失败:", error);
|
||||
alert(t('styleCustomizer.uiThemeResetFailed', { message: error.message }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 辅助函数:将 CSS 变量名转换为更友好的标签
|
||||
// 保存终端字体
|
||||
const handleSaveTerminalFont = async () => {
|
||||
try {
|
||||
await appearanceStore.setTerminalFontFamily(editableTerminalFontFamily.value);
|
||||
alert(t('styleCustomizer.terminalFontSaved'));
|
||||
} catch (error: any) {
|
||||
console.error("保存终端字体失败:", error);
|
||||
alert(t('styleCustomizer.terminalFontSaveFailed', { message: error.message }));
|
||||
}
|
||||
};
|
||||
|
||||
// 更改激活的终端主题
|
||||
const handleTerminalThemeChange = async () => {
|
||||
try {
|
||||
await appearanceStore.setActiveTerminalTheme(selectedTerminalThemeId.value);
|
||||
} catch (error: any) {
|
||||
console.error("设置激活终端主题失败:", error);
|
||||
// 恢复下拉框选择到之前的状态
|
||||
selectedTerminalThemeId.value = activeTerminalThemeId.value ?? null;
|
||||
alert(t('styleCustomizer.setActiveThemeFailed', { message: error.message }));
|
||||
}
|
||||
};
|
||||
|
||||
// --- 终端主题管理 ---
|
||||
// 开始新建主题
|
||||
const handleAddNewTheme = () => {
|
||||
saveThemeError.value = null; // 清除旧错误
|
||||
// 创建一个全新的默认主题结构用于编辑
|
||||
editingTheme.value = {
|
||||
_id: undefined, // 清除 ID 表示是新建
|
||||
name: t('styleCustomizer.newThemeDefaultName'),
|
||||
themeData: JSON.parse(JSON.stringify(defaultXtermTheme)), // 使用默认 xterm 主题作为基础
|
||||
isPreset: false, // 明确不是预设
|
||||
};
|
||||
isEditingTheme.value = true;
|
||||
};
|
||||
|
||||
|
||||
// 开始编辑现有主题
|
||||
const handleEditTheme = (theme: TerminalTheme) => {
|
||||
if (theme.isPreset) return; // 不允许编辑预设
|
||||
saveThemeError.value = null; // 清除旧错误
|
||||
// 深拷贝以避免直接修改列表中的对象
|
||||
editingTheme.value = JSON.parse(JSON.stringify(theme));
|
||||
isEditingTheme.value = true;
|
||||
};
|
||||
|
||||
// 保存主题编辑 (新建或更新)
|
||||
const handleSaveEditingTheme = async () => {
|
||||
if (!editingTheme.value || !editingTheme.value.name) {
|
||||
saveThemeError.value = t('styleCustomizer.errorThemeNameRequired');
|
||||
return;
|
||||
}
|
||||
saveThemeError.value = null; // 清除错误
|
||||
try {
|
||||
if (editingTheme.value._id) { // 更新
|
||||
// 确保传递的是 UpdateTerminalThemeDto 兼容的格式
|
||||
const updateDto = { name: editingTheme.value.name, themeData: editingTheme.value.themeData };
|
||||
await appearanceStore.updateTerminalTheme(
|
||||
editingTheme.value._id,
|
||||
updateDto.name,
|
||||
updateDto.themeData
|
||||
);
|
||||
alert(t('styleCustomizer.themeUpdatedSuccess'));
|
||||
} else { // 新建
|
||||
// 确保传递的是 CreateTerminalThemeDto 兼容的格式
|
||||
const createDto = { name: editingTheme.value.name, themeData: editingTheme.value.themeData };
|
||||
await appearanceStore.createTerminalTheme(
|
||||
createDto.name,
|
||||
createDto.themeData
|
||||
);
|
||||
alert(t('styleCustomizer.themeCreatedSuccess'));
|
||||
}
|
||||
isEditingTheme.value = false; // 关闭编辑
|
||||
editingTheme.value = null;
|
||||
} catch (error: any) {
|
||||
console.error("保存终端主题失败:", error);
|
||||
saveThemeError.value = error.message || t('styleCustomizer.themeSaveFailed');
|
||||
}
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const handleCancelEditingTheme = () => {
|
||||
isEditingTheme.value = false;
|
||||
editingTheme.value = null;
|
||||
saveThemeError.value = null;
|
||||
};
|
||||
|
||||
// 删除主题
|
||||
const handleDeleteTheme = async (theme: TerminalTheme) => {
|
||||
if (theme.isPreset) return;
|
||||
if (confirm(t('styleCustomizer.confirmDeleteTheme', { name: theme.name }))) {
|
||||
try {
|
||||
await appearanceStore.deleteTerminalTheme(theme._id!);
|
||||
alert(t('styleCustomizer.themeDeletedSuccess'));
|
||||
} catch (error: any) {
|
||||
console.error("删除终端主题失败:", error);
|
||||
alert(t('styleCustomizer.themeDeleteFailed', { message: error.message }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 触发主题导入文件选择
|
||||
const handleTriggerImport = () => {
|
||||
importError.value = null;
|
||||
themeImportInput.value?.click();
|
||||
};
|
||||
|
||||
// 处理主题导入
|
||||
const handleImportThemeFile = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
try {
|
||||
// 可以选择在前端解析文件名作为默认名称传递给后端
|
||||
const defaultName = file.name.endsWith('.json') ? file.name.slice(0, -5) : file.name;
|
||||
await appearanceStore.importTerminalTheme(file, defaultName); // 传递文件名作为备选名称
|
||||
alert(t('styleCustomizer.importSuccess'));
|
||||
input.value = ''; // 清空文件输入,以便再次选择相同文件
|
||||
} catch (error: any) {
|
||||
console.error("导入主题失败:", error);
|
||||
importError.value = error.message || t('styleCustomizer.importFailed');
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理主题导出
|
||||
const handleExportTheme = async () => {
|
||||
if (selectedTerminalThemeId.value) {
|
||||
try {
|
||||
await appearanceStore.exportTerminalTheme(selectedTerminalThemeId.value);
|
||||
} catch (error: any) {
|
||||
console.error("导出主题失败:", error);
|
||||
alert(t('styleCustomizer.exportFailed', { message: error.message }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- 背景处理 ---
|
||||
const handleTriggerPageBgUpload = () => {
|
||||
uploadError.value = null;
|
||||
pageBgFileInput.value?.click();
|
||||
};
|
||||
const handleTriggerTerminalBgUpload = () => {
|
||||
uploadError.value = null;
|
||||
terminalBgFileInput.value?.click();
|
||||
};
|
||||
|
||||
const handlePageBgUpload = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
try {
|
||||
await appearanceStore.uploadPageBackground(file);
|
||||
alert(t('styleCustomizer.pageBgUploadSuccess'));
|
||||
input.value = ''; // 清空以便再次选择
|
||||
} catch (error: any) {
|
||||
uploadError.value = error.message || t('styleCustomizer.uploadFailed');
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTerminalBgUpload = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0];
|
||||
try {
|
||||
await appearanceStore.uploadTerminalBackground(file);
|
||||
alert(t('styleCustomizer.terminalBgUploadSuccess'));
|
||||
input.value = '';
|
||||
} catch (error: any) {
|
||||
uploadError.value = error.message || t('styleCustomizer.uploadFailed');
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemovePageBg = async () => {
|
||||
if (confirm(t('styleCustomizer.confirmRemovePageBg'))) {
|
||||
try {
|
||||
await appearanceStore.removePageBackground();
|
||||
alert(t('styleCustomizer.pageBgRemoved'));
|
||||
} catch (error: any) {
|
||||
console.error("移除页面背景失败:", error);
|
||||
alert(t('styleCustomizer.removeBgFailed', { message: error.message }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTerminalBg = async () => {
|
||||
if (confirm(t('styleCustomizer.confirmRemoveTerminalBg'))) {
|
||||
try {
|
||||
await appearanceStore.removeTerminalBackground();
|
||||
alert(t('styleCustomizer.terminalBgRemoved'));
|
||||
} catch (error: any) {
|
||||
console.error("移除终端背景失败:", error);
|
||||
alert(t('styleCustomizer.removeBgFailed', { message: error.message }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageOpacityChange = async () => {
|
||||
try {
|
||||
await appearanceStore.setPageBackgroundOpacity(editablePageBackgroundOpacity.value);
|
||||
} catch (error: any) {
|
||||
console.error("设置页面背景透明度失败:", error);
|
||||
// 恢复旧值
|
||||
editablePageBackgroundOpacity.value = pageBackgroundOpacity.value;
|
||||
alert(t('styleCustomizer.setOpacityFailed', { message: error.message }));
|
||||
}
|
||||
};
|
||||
const handleTerminalOpacityChange = async () => {
|
||||
try {
|
||||
await appearanceStore.setTerminalBackgroundOpacity(editableTerminalBackgroundOpacity.value);
|
||||
} catch (error: any) {
|
||||
console.error("设置终端背景透明度失败:", error);
|
||||
editableTerminalBackgroundOpacity.value = terminalBackgroundOpacity.value;
|
||||
alert(t('styleCustomizer.setOpacityFailed', { message: error.message }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- 辅助函数 ---
|
||||
// 格式化 UI 主题标签
|
||||
const formatLabel = (key: string): string => {
|
||||
// 简单的转换逻辑,可以根据需要优化
|
||||
return key
|
||||
@@ -69,12 +360,11 @@ const formatLabel = (key: string): string => {
|
||||
.replace(/^./, (str) => str.toUpperCase()); // 首字母大写
|
||||
};
|
||||
|
||||
// 辅助函数:将 xterm theme key 转换为更友好的标签
|
||||
// 格式化 xterm 主题属性标签
|
||||
const formatXtermLabel = (key: keyof ITheme): string => {
|
||||
// 简单的转换逻辑
|
||||
return key.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase());
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -89,9 +379,12 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
||||
<button @click="currentTab = 'ui'" :class="{ active: currentTab === 'ui' }">
|
||||
{{ t('styleCustomizer.uiStyles') }}
|
||||
</button>
|
||||
<button @click="currentTab = 'terminal'" :class="{ active: currentTab === 'terminal' }">
|
||||
<button @click="currentTab = 'terminal'" :class="{ active: currentTab === 'terminal' && !isEditingTheme }" :disabled="isEditingTheme">
|
||||
{{ t('styleCustomizer.terminalStyles') }}
|
||||
</button>
|
||||
<button @click="currentTab = 'background'" :class="{ active: currentTab === 'background' }" :disabled="isEditingTheme">
|
||||
{{ t('styleCustomizer.backgroundSettings') }}
|
||||
</button>
|
||||
</nav>
|
||||
<main class="panel-main">
|
||||
<section v-if="currentTab === 'ui'">
|
||||
@@ -117,34 +410,132 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="currentTab === 'terminal'">
|
||||
<section v-if="currentTab === 'terminal' && !isEditingTheme">
|
||||
<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 class="form-group">
|
||||
<label for="terminalFontFamily">{{ t('styleCustomizer.terminalFontFamily') }}:</label>
|
||||
<input type="text" id="terminalFontFamily" v-model="editableTerminalFontFamily" class="text-input wide-input" :placeholder="t('styleCustomizer.terminalFontPlaceholder')"/>
|
||||
<button @click="handleSaveTerminalFont" class="button-inline">{{ t('common.save') }}</button>
|
||||
</div>
|
||||
<p class="setting-description">{{ t('styleCustomizer.terminalFontDescription') }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- 终端主题选择与管理 -->
|
||||
<h4>{{ t('styleCustomizer.terminalThemeSelection') }}</h4>
|
||||
<div class="form-group">
|
||||
<label for="terminalThemeSelect">{{ t('styleCustomizer.activeTheme') }}:</label>
|
||||
<select id="terminalThemeSelect" v-model="selectedTerminalThemeId" @change="handleTerminalThemeChange">
|
||||
<option :value="null">{{ t('styleCustomizer.selectThemePrompt') }}</option> <!-- 添加一个空选项或默认选项 -->
|
||||
<option v-for="theme in availableTerminalThemes" :key="theme._id" :value="theme._id">
|
||||
{{ theme.name }} {{ theme.isPreset ? `(${t('styleCustomizer.preset')})` : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="handleExportTheme" :disabled="!selectedTerminalThemeId" class="button-inline">{{ t('styleCustomizer.exportTheme') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="theme-management-buttons">
|
||||
<button @click="handleAddNewTheme">{{ t('styleCustomizer.addNewTheme') }}</button>
|
||||
<button @click="handleTriggerImport">{{ t('styleCustomizer.importTheme') }}</button>
|
||||
<input type="file" ref="themeImportInput" @change="handleImportThemeFile" accept=".json" style="display: none;" />
|
||||
<p v-if="importError" class="error-message">{{ importError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 主题列表 -->
|
||||
<ul class="theme-list">
|
||||
<li v-for="theme in availableTerminalThemes" :key="theme._id" :class="{ 'preset-theme': theme.isPreset }">
|
||||
<span>{{ theme.name }} {{ theme.isPreset ? `(${t('styleCustomizer.preset')})` : '' }}</span>
|
||||
<div class="theme-actions">
|
||||
<button @click="handleEditTheme(theme)" :disabled="theme.isPreset">{{ t('common.edit') }}</button>
|
||||
<button @click="handleDeleteTheme(theme)" :disabled="theme.isPreset" class="button-danger">{{ t('common.delete') }}</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- 主题编辑器 -->
|
||||
<section v-if="isEditingTheme && editingTheme">
|
||||
<h3>{{ editingTheme._id ? t('styleCustomizer.editThemeTitle') : t('styleCustomizer.newThemeTitle') }}</h3>
|
||||
<p v-if="saveThemeError" class="error-message">{{ saveThemeError }}</p>
|
||||
<div class="form-group">
|
||||
<label for="editingThemeName">{{ t('styleCustomizer.themeName') }}:</label>
|
||||
<input type="text" id="editingThemeName" v-model="editingTheme.name" required class="text-input"/>
|
||||
</div>
|
||||
<!-- 动态生成终端样式编辑控件 -->
|
||||
<div v-for="(value, key) in editingTheme.themeData" :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="(editingTheme.themeData as any)[key]"
|
||||
/>
|
||||
<!-- 其他类型(如数字、布尔值)可以添加相应控件,这里简化为文本 -->
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
:id="`xterm-${key}`"
|
||||
v-model="(editingTheme.themeData as any)[key]"
|
||||
class="text-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="editor-footer">
|
||||
<button @click="handleCancelEditingTheme" class="button-secondary">{{ t('common.cancel') }}</button>
|
||||
<button @click="handleSaveEditingTheme" class="button-primary">{{ t('common.save') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="currentTab === 'background'">
|
||||
<h3>{{ t('styleCustomizer.backgroundSettings') }}</h3>
|
||||
|
||||
<!-- 页面背景 -->
|
||||
<h4>{{ t('styleCustomizer.pageBackground') }}</h4>
|
||||
<div class="background-preview" :style="{ backgroundImage: pageBackgroundImage ? `url(${pageBackgroundImage})` : 'none' }">
|
||||
{{ pageBackgroundImage ? '' : t('styleCustomizer.noBackground') }}
|
||||
</div>
|
||||
<div class="background-controls">
|
||||
<button @click="handleTriggerPageBgUpload">{{ t('styleCustomizer.uploadPageBg') }}</button>
|
||||
<button @click="handleRemovePageBg" :disabled="!pageBackgroundImage" class="button-danger">{{ t('styleCustomizer.removePageBg') }}</button>
|
||||
<input type="file" ref="pageBgFileInput" @change="handlePageBgUpload" accept="image/*" style="display: none;" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pageBgOpacity">{{ t('styleCustomizer.pageBgOpacity') }}:</label>
|
||||
<input type="range" id="pageBgOpacity" v-model.number="editablePageBackgroundOpacity" min="0" max="1" step="0.05" @change="handlePageOpacityChange"/>
|
||||
<span>{{ Math.round(editablePageBackgroundOpacity * 100) }}%</span>
|
||||
</div>
|
||||
<p v-if="uploadError" class="error-message">{{ uploadError }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- 终端背景 -->
|
||||
<h4>{{ t('styleCustomizer.terminalBackground') }}</h4>
|
||||
<div class="background-preview" :style="{ backgroundImage: terminalBackgroundImage ? `url(${terminalBackgroundImage})` : 'none' }">
|
||||
{{ terminalBackgroundImage ? '' : t('styleCustomizer.noBackground') }}
|
||||
</div>
|
||||
<div class="background-controls">
|
||||
<button @click="handleTriggerTerminalBgUpload">{{ t('styleCustomizer.uploadTerminalBg') }}</button>
|
||||
<button @click="handleRemoveTerminalBg" :disabled="!terminalBackgroundImage" class="button-danger">{{ t('styleCustomizer.removeTerminalBg') }}</button>
|
||||
<input type="file" ref="terminalBgFileInput" @change="handleTerminalBgUpload" accept="image/*" style="display: none;" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="terminalBgOpacity">{{ t('styleCustomizer.terminalBgOpacity') }}:</label>
|
||||
<input type="range" id="terminalBgOpacity" v-model.number="editableTerminalBackgroundOpacity" min="0" max="1" step="0.05" @change="handleTerminalOpacityChange"/>
|
||||
<span>{{ Math.round(editableTerminalBackgroundOpacity * 100) }}%</span>
|
||||
</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>
|
||||
<!-- 根据当前 tab 或状态显示不同的按钮 -->
|
||||
<button v-if="currentTab === 'ui'" @click="handleResetUiTheme" class="button-secondary">{{ t('styleCustomizer.resetUiTheme') }}</button>
|
||||
<button v-if="currentTab === 'ui'" @click="handleSaveUiTheme" class="button-primary">{{ t('styleCustomizer.saveUiTheme') }}</button>
|
||||
<!-- 终端字体和主题选择是即时保存的,不需要单独的保存按钮 -->
|
||||
<!-- 背景设置也是即时保存的 -->
|
||||
<button @click="closeCustomizer" class="button-secondary">{{ t('common.close') }}</button> <!-- 添加一个通用的关闭按钮 -->
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,6 +601,7 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
||||
border-right: 1px solid var(--border-color, #ccc);
|
||||
padding: var(--base-padding, 1rem);
|
||||
background-color: var(--header-bg-color, #f0f0f0); /* 轻微区分背景 */
|
||||
flex-shrink: 0; /* 防止导航栏被压缩 */
|
||||
}
|
||||
|
||||
.panel-nav button {
|
||||
@@ -234,6 +626,13 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
||||
color: var(--button-text-color, #fff);
|
||||
font-weight: bold;
|
||||
}
|
||||
.panel-nav button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background-color: transparent !important; /* 确保禁用时背景透明 */
|
||||
color: var(--text-color-secondary, #999); /* 禁用时文字颜色变灰 */
|
||||
}
|
||||
|
||||
|
||||
.panel-main {
|
||||
flex-grow: 1;
|
||||
@@ -247,6 +646,11 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.panel-main h4 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.8rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.panel-main p {
|
||||
color: var(--text-color-secondary);
|
||||
@@ -254,34 +658,65 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
.setting-description {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-color-secondary);
|
||||
margin-top: -0.5rem; /* 减少与上方元素的间距 */
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
display: flex; /* 使用 flex 布局 */
|
||||
align-items: center; /* 垂直居中对齐 */
|
||||
flex-wrap: wrap; /* 允许换行 */
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: inline-block;
|
||||
/* display: inline-block; */
|
||||
min-width: 150px; /* 调整标签最小宽度以适应更长的文本 */
|
||||
margin-right: 0.5rem;
|
||||
vertical-align: middle;
|
||||
/* vertical-align: middle; */
|
||||
text-align: right; /* 标签右对齐 */
|
||||
padding-right: 5px; /* 标签和输入框间距 */
|
||||
flex-shrink: 0; /* 防止标签被压缩 */
|
||||
}
|
||||
|
||||
.form-group input[type="color"] {
|
||||
vertical-align: middle;
|
||||
/* vertical-align: middle; */
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
width: 150px; /* 统一输入框宽度 */
|
||||
height: 30px; /* 增加高度 */
|
||||
}
|
||||
|
||||
.form-group input[type="text"].text-input {
|
||||
vertical-align: middle;
|
||||
/* vertical-align: middle; */
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
width: 150px; /* 统一文本输入框宽度 */
|
||||
flex-grow: 1; /* 允许输入框扩展 */
|
||||
min-width: 100px; /* 最小宽度 */
|
||||
}
|
||||
.form-group input[type="text"].wide-input {
|
||||
/* width: calc(100% - 200px); */ /* 调整宽度以适应按钮 */
|
||||
/* min-width: 200px; */
|
||||
flex-grow: 1; /* 占据更多空间 */
|
||||
}
|
||||
.form-group select {
|
||||
padding: 4px 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
flex-grow: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
.form-group input[type="range"] {
|
||||
flex-grow: 1;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
|
||||
.panel-footer {
|
||||
display: flex;
|
||||
@@ -318,4 +753,113 @@ const formatXtermLabel = (key: keyof ITheme): string => {
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
|
||||
.button-inline {
|
||||
margin-left: 10px;
|
||||
padding: 4px 8px;
|
||||
/* vertical-align: middle; */
|
||||
flex-shrink: 0; /* 防止按钮被压缩 */
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color, #eee);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Theme Management Styles */
|
||||
.theme-management-buttons {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
gap: 10px; /* 按钮间距 */
|
||||
flex-wrap: wrap; /* 允许换行 */
|
||||
}
|
||||
.theme-management-buttons button {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.theme-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
max-height: 200px; /* 限制列表高度并滚动 */
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.theme-list li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.theme-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.theme-list li.preset-theme span {
|
||||
font-style: italic;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.theme-actions {
|
||||
flex-shrink: 0; /* 防止按钮组被压缩 */
|
||||
}
|
||||
.theme-actions button {
|
||||
margin-left: 0.5rem;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.button-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
.button-danger:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
}
|
||||
.button-danger:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.editor-footer {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Background Styles */
|
||||
.background-preview {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border: 1px dashed var(--border-color);
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-color-secondary);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.background-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.background-controls button {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: red;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 5px;
|
||||
width: 100%; /* 确保错误消息占满宽度 */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
import { ITheme } from 'xterm';
|
||||
import { Terminal } from 'xterm';
|
||||
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 store
|
||||
import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 store
|
||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
@@ -27,25 +27,25 @@ let terminal: Terminal | null = null;
|
||||
let fitAddon: FitAddon | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let debounceTimer: number | null = null; // 用于防抖的计时器 ID
|
||||
const fontSize = ref(14); // 字体大小状态, 默认为14
|
||||
const fontSize = ref(14); // 字体大小状态, 默认为14 (这个可以保留,或者也移到 appearance store)
|
||||
|
||||
// --- Settings Store ---
|
||||
const settingsStore = useSettingsStore();
|
||||
const { currentXtermTheme } = storeToRefs(settingsStore); // 获取响应式的 xterm 主题
|
||||
// --- Appearance Store ---
|
||||
const appearanceStore = useAppearanceStore();
|
||||
const { currentTerminalTheme, currentTerminalFontFamily, terminalBackgroundImage, terminalBackgroundOpacity } = storeToRefs(appearanceStore); // 获取外观状态
|
||||
|
||||
// 防抖函数
|
||||
const debounce = (func: Function, delay: number) => {
|
||||
let timeoutId: number | null = null; // Use a local variable for the timeout ID
|
||||
return (...args: any[]) => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
debounceTimer = window.setTimeout(() => {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
func(...args);
|
||||
debounceTimer = null;
|
||||
timeoutId = null;
|
||||
}, delay);
|
||||
};
|
||||
};
|
||||
|
||||
// 防抖处理由 ResizeObserver 触发的 resize 事件
|
||||
const debouncedEmitResize = debounce((term: Terminal) => {
|
||||
if (term && props.isActive) { // 仅当标签仍处于活动状态时才发送防抖后的 resize
|
||||
@@ -75,9 +75,9 @@ onMounted(() => {
|
||||
if (terminalRef.value) {
|
||||
terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: fontSize.value,
|
||||
fontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"',
|
||||
theme: currentXtermTheme.value, // *** 使用 store 中的当前 xterm 主题 ***
|
||||
fontSize: fontSize.value, // 初始字体大小
|
||||
fontFamily: currentTerminalFontFamily.value, // 使用 store 中的字体设置
|
||||
theme: currentTerminalTheme.value, // 使用 store 中的当前 xterm 主题
|
||||
rows: 24, // 初始行数
|
||||
cols: 80, // 初始列数
|
||||
allowTransparency: true,
|
||||
@@ -185,19 +185,35 @@ onMounted(() => {
|
||||
emit('ready', { sessionId: props.sessionId, terminal: terminal });
|
||||
}
|
||||
|
||||
// --- 监听 xterm 主题变化 ---
|
||||
watch(currentXtermTheme, (newTheme) => {
|
||||
// --- 监听外观变化 ---
|
||||
watch(currentTerminalTheme, (newTheme) => {
|
||||
if (terminal) {
|
||||
console.log(`[Terminal ${props.sessionId}] Applying new xterm theme.`); // 日志改为中文
|
||||
console.log(`[Terminal ${props.sessionId}] 应用新终端主题。`);
|
||||
terminal.options.theme = newTheme;
|
||||
// 可能需要重新渲染或刷新终端以完全应用主题,但通常 xterm 会自动处理
|
||||
// terminal.refresh(0, terminal.rows - 1); // 如果需要强制刷新
|
||||
}
|
||||
}, { deep: true }); // 使用 deep watch
|
||||
}, { deep: true });
|
||||
|
||||
// 聚焦终端
|
||||
terminal.focus();
|
||||
|
||||
watch(currentTerminalFontFamily, (newFontFamily) => {
|
||||
if (terminal) {
|
||||
console.log(`[Terminal ${props.sessionId}] 应用新终端字体: ${newFontFamily}`);
|
||||
terminal.options.fontFamily = newFontFamily;
|
||||
// 字体变化可能影响尺寸,重新 fit
|
||||
fitAndEmitResizeNow(terminal);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听背景图片和透明度 (恢复之前的监听方式,因为监听整个对象可能引入其他问题)
|
||||
watch([terminalBackgroundImage, terminalBackgroundOpacity], () => {
|
||||
console.log(`[Terminal Watcher] terminalBackgroundImage or Opacity changed. New image: ${terminalBackgroundImage.value}`); // 添加日志确认 watcher 触发
|
||||
applyTerminalBackground();
|
||||
}, { immediate: true }); // 添加 immediate: true,强制立即执行一次
|
||||
// 移除 onMounted 中的 applyTerminalBackground 调用,完全依赖 watch
|
||||
// applyTerminalBackground(); // 初始应用一次
|
||||
|
||||
// 聚焦终端 (添加 null check)
|
||||
if (terminal) {
|
||||
terminal.focus();
|
||||
}
|
||||
// 重新添加鼠标滚轮缩放功能
|
||||
if (terminalRef.value) {
|
||||
terminalRef.value.addEventListener('wheel', (event: WheelEvent) => {
|
||||
@@ -264,6 +280,53 @@ const write = (data: string | Uint8Array) => {
|
||||
};
|
||||
defineExpose({ write });
|
||||
|
||||
// --- 应用终端背景 ---
|
||||
const applyTerminalBackground = () => {
|
||||
if (terminalRef.value) {
|
||||
if (terminalBackgroundImage.value) {
|
||||
// --- 修改开始 ---
|
||||
// 使用环境变量获取后端基础 URL
|
||||
const backendUrl = import.meta.env.VITE_API_BASE_URL || ''; // 提供一个默认空字符串以防万一
|
||||
const imagePath = terminalBackgroundImage.value;
|
||||
console.log(`[Terminal applyTerminalBackground] backendUrl: "${backendUrl}", imagePath: "${imagePath}"`); // 详细日志
|
||||
const fullImageUrl = `${backendUrl}${imagePath}`;
|
||||
console.log(`[Terminal applyTerminalBackground] fullImageUrl: "${fullImageUrl}"`); // 打印完整 URL
|
||||
// --- 修改结束 ---
|
||||
// --- 使用 nextTick 包装样式应用 ---
|
||||
nextTick(() => {
|
||||
if (terminalRef.value) { // 再次检查 ref 是否存在
|
||||
terminalRef.value.style.backgroundImage = `url(${fullImageUrl})`;
|
||||
terminalRef.value.style.backgroundSize = 'cover'; // Or 'contain', 'auto', etc.
|
||||
terminalRef.value.style.backgroundPosition = 'center';
|
||||
terminalRef.value.style.backgroundRepeat = 'no-repeat';
|
||||
// 添加 CSS 类
|
||||
terminalRef.value.classList.add('has-terminal-background');
|
||||
}
|
||||
});
|
||||
// 应用透明度: 通过设置背景色实现,需要 xterm 的 allowTransparency: true
|
||||
// 注意:这会影响整个终端的背景,包括文本后的背景
|
||||
// 一个常见的做法是设置一个稍微透明的背景色,让图片透出来
|
||||
// 例如,将 xterm 主题的 background 设置为 rgba(r, g, b, opacity)
|
||||
// 这里我们简单设置容器的 opacity,但这会影响文本!更好的方法是修改主题。
|
||||
// 另一种方法是用伪元素做背景层。
|
||||
// 为了简单起见,我们暂时只设置背景图,透明度让用户在主题中调整 background 的 alpha 值。
|
||||
// terminalRef.value.style.opacity = terminalBackgroundOpacity.value.toString(); // 不推荐直接设置 opacity
|
||||
console.log(`[Terminal ${props.sessionId}] 应用终端背景图片: ${terminalBackgroundImage.value}`);
|
||||
} else {
|
||||
// --- 使用 nextTick 包装样式移除 ---
|
||||
nextTick(() => {
|
||||
if (terminalRef.value) { // 再次检查 ref 是否存在
|
||||
terminalRef.value.style.backgroundImage = 'none';
|
||||
// 移除 CSS 类
|
||||
terminalRef.value.classList.remove('has-terminal-background');
|
||||
}
|
||||
});
|
||||
// terminalRef.value.style.opacity = '1'; // 移除背景时恢复不透明
|
||||
console.log(`[Terminal ${props.sessionId}] 移除终端背景图片。`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -276,7 +339,32 @@ defineExpose({ write });
|
||||
width: 100%;
|
||||
height: 100%; /* 高度需要由父容器控制 */
|
||||
overflow: hidden; /* 阻止此容器本身产生滚动条 */
|
||||
position: relative; /* 用于可能的伪元素背景 */
|
||||
}
|
||||
|
||||
/* 移除 :deep 样式,让 xterm 内部自然处理滚动 */
|
||||
|
||||
/* 示例:使用伪元素添加带透明度的背景层 (如果需要独立于主题的透明度) */
|
||||
/*
|
||||
.terminal-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: var(--terminal-bg-image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
opacity: var(--terminal-bg-opacity);
|
||||
z-index: -1; // 确保在 xterm 内容后面
|
||||
}
|
||||
*/
|
||||
|
||||
/* 当容器有背景图时,强制内部 xterm 视口和屏幕背景透明 */
|
||||
.terminal-container.has-terminal-background :deep(.xterm-viewport),
|
||||
.terminal-container.has-terminal-background :deep(.xterm-screen) {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user