@@ -19,6 +19,7 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
|
|||||||
const settings: Partial<AppearanceSettings> = {};
|
const settings: Partial<AppearanceSettings> = {};
|
||||||
let latestUpdatedAt = 0;
|
let latestUpdatedAt = 0;
|
||||||
let terminalBackgroundEnabledFound = false; // 标记是否在数据库中找到该设置
|
let terminalBackgroundEnabledFound = false; // 标记是否在数据库中找到该设置
|
||||||
|
let terminalBackgroundOverlayOpacityFound = false; // 标记是否找到蒙版透明度设置
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
// 更新 latestUpdatedAt
|
// 更新 latestUpdatedAt
|
||||||
@@ -51,7 +52,11 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
|
|||||||
break;
|
break;
|
||||||
case 'terminalBackgroundEnabled':
|
case 'terminalBackgroundEnabled':
|
||||||
settings.terminalBackgroundEnabled = row.value === 'true'; // 将 'true'/'false' 字符串转为 boolean
|
settings.terminalBackgroundEnabled = row.value === 'true'; // 将 'true'/'false' 字符串转为 boolean
|
||||||
terminalBackgroundEnabledFound = true; // 标记已找到
|
terminalBackgroundEnabledFound = true;
|
||||||
|
break;
|
||||||
|
case 'terminalBackgroundOverlayOpacity':
|
||||||
|
settings.terminalBackgroundOverlayOpacity = parseFloat(row.value);
|
||||||
|
terminalBackgroundOverlayOpacityFound = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,6 +75,9 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
|
|||||||
terminalBackgroundEnabled: terminalBackgroundEnabledFound
|
terminalBackgroundEnabled: terminalBackgroundEnabledFound
|
||||||
? settings.terminalBackgroundEnabled // 使用数据库找到的值 (true 或 false)
|
? settings.terminalBackgroundEnabled // 使用数据库找到的值 (true 或 false)
|
||||||
: defaults.terminalBackgroundEnabled, // 否则使用默认值 (true)
|
: defaults.terminalBackgroundEnabled, // 否则使用默认值 (true)
|
||||||
|
terminalBackgroundOverlayOpacity: terminalBackgroundOverlayOpacityFound
|
||||||
|
? settings.terminalBackgroundOverlayOpacity // 使用数据库找到的值
|
||||||
|
: defaults.terminalBackgroundOverlayOpacity, // 否则使用默认值
|
||||||
updatedAt: latestUpdatedAt || defaults.updatedAt, // 使用最新的更新时间,否则使用默认时间戳
|
updatedAt: latestUpdatedAt || defaults.updatedAt, // 使用最新的更新时间,否则使用默认时间戳
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -86,6 +94,7 @@ const getDefaultAppearanceSettings = (): Omit<AppearanceSettings, '_id'> => {
|
|||||||
terminalBackgroundImage: undefined,
|
terminalBackgroundImage: undefined,
|
||||||
pageBackgroundImage: undefined,
|
pageBackgroundImage: undefined,
|
||||||
terminalBackgroundEnabled: true, // 默认启用
|
terminalBackgroundEnabled: true, // 默认启用
|
||||||
|
terminalBackgroundOverlayOpacity: 0.5, // 默认蒙版透明度
|
||||||
updatedAt: Date.now(), // 提供默认时间戳
|
updatedAt: Date.now(), // 提供默认时间戳
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -110,7 +119,8 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
|
|||||||
{ key: 'editorFontSize', value: defaults.editorFontSize },
|
{ key: 'editorFontSize', value: defaults.editorFontSize },
|
||||||
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // 数据库中使用空字符串
|
{ key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // 数据库中使用空字符串
|
||||||
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // 数据库中使用空字符串
|
{ key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // 数据库中使用空字符串
|
||||||
{ key: 'terminalBackgroundEnabled', value: defaults.terminalBackgroundEnabled }, // 新增
|
{ key: 'terminalBackgroundEnabled', value: defaults.terminalBackgroundEnabled },
|
||||||
|
{ key: 'terminalBackgroundOverlayOpacity', value: defaults.terminalBackgroundOverlayOpacity }, // 新增
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import * as terminalThemeRepository from '../repositories/terminal-theme.reposit
|
|||||||
* @returns Promise<AppearanceSettings>
|
* @returns Promise<AppearanceSettings>
|
||||||
*/
|
*/
|
||||||
export const getSettings = async (): Promise<AppearanceSettings> => {
|
export const getSettings = async (): Promise<AppearanceSettings> => {
|
||||||
return appearanceRepository.getAppearanceSettings();
|
const settings = await appearanceRepository.getAppearanceSettings();
|
||||||
|
// 为 terminalBackgroundOverlayOpacity 提供默认值
|
||||||
|
if (settings.terminalBackgroundOverlayOpacity === undefined || settings.terminalBackgroundOverlayOpacity === null) {
|
||||||
|
settings.terminalBackgroundOverlayOpacity = 0.5; // 默认透明度为 0.5
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +69,15 @@ export const updateSettings = async (settingsDto: UpdateAppearanceDto): Promise<
|
|||||||
settingsDto.editorFontSize = size;
|
settingsDto.editorFontSize = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证 terminalBackgroundOverlayOpacity (如果提供了)
|
||||||
|
if (settingsDto.terminalBackgroundOverlayOpacity !== undefined && settingsDto.terminalBackgroundOverlayOpacity !== null) {
|
||||||
|
const opacity = Number(settingsDto.terminalBackgroundOverlayOpacity);
|
||||||
|
if (isNaN(opacity) || opacity < 0 || opacity > 1) {
|
||||||
|
throw new Error(`无效的终端背景蒙版透明度: ${settingsDto.terminalBackgroundOverlayOpacity}。必须是一个 0 到 1 之间的数字。`);
|
||||||
|
}
|
||||||
|
settingsDto.terminalBackgroundOverlayOpacity = opacity; // 确保类型正确
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: 如果实现了背景图片上传,这里需要处理文件路径或 URL 的验证/保存逻辑
|
// TODO: 如果实现了背景图片上传,这里需要处理文件路径或 URL 的验证/保存逻辑
|
||||||
|
|
||||||
return appearanceRepository.updateAppearanceSettings(settingsDto);
|
return appearanceRepository.updateAppearanceSettings(settingsDto);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface AppearanceSettings {
|
|||||||
pageBackgroundImage?: string; // 页面背景图片 URL 或路径
|
pageBackgroundImage?: string; // 页面背景图片 URL 或路径
|
||||||
editorFontSize?: number; // 编辑器字体大小 (px)
|
editorFontSize?: number; // 编辑器字体大小 (px)
|
||||||
terminalBackgroundEnabled?: boolean; // 终端背景是否启用
|
terminalBackgroundEnabled?: boolean; // 终端背景是否启用
|
||||||
|
terminalBackgroundOverlayOpacity?: number; // 终端背景蒙版透明度 (0-1)
|
||||||
updatedAt?: number;
|
updatedAt?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export interface UpdateCaptchaSettingsDto {
|
|||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
sidebar?: SidebarConfig;
|
sidebar?: SidebarConfig;
|
||||||
captcha?: CaptchaSettings;
|
captcha?: CaptchaSettings;
|
||||||
showStatusMonitorIpAddress?: boolean; // 新增:是否在状态监视器中显示IP地址
|
showStatusMonitorIpAddress?: boolean; // 是否在状态监视器中显示IP地址
|
||||||
// 可以添加其他设置模块,例如:
|
// 可以添加其他设置模块,例如:
|
||||||
// security?: SecuritySettings;
|
// security?: SecuritySettings;
|
||||||
// general?: GeneralSettings;
|
// general?: GeneralSettings;
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ const {
|
|||||||
|
|
||||||
terminalBackgroundImage,
|
terminalBackgroundImage,
|
||||||
|
|
||||||
isTerminalBackgroundEnabled,
|
isTerminalBackgroundEnabled,
|
||||||
|
currentTerminalBackgroundOverlayOpacity, // 从 store 解构
|
||||||
} = storeToRefs(appearanceStore);
|
} = storeToRefs(appearanceStore);
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +35,8 @@ const editableEditorFontSize = ref(14);
|
|||||||
|
|
||||||
const editableUiThemeString = ref('');
|
const editableUiThemeString = ref('');
|
||||||
const themeParseError = ref<string | null>(null);
|
const themeParseError = ref<string | null>(null);
|
||||||
const localTerminalBackgroundEnabled = ref(true);
|
const localTerminalBackgroundEnabled = ref(true);
|
||||||
|
const editableTerminalBackgroundOverlayOpacity = ref(0.5); // 本地编辑状态,默认 0.5
|
||||||
|
|
||||||
// 终端主题管理相关状态
|
// 终端主题管理相关状态
|
||||||
const isEditingTheme = ref(false); // 是否正在编辑某个主题
|
const isEditingTheme = ref(false); // 是否正在编辑某个主题
|
||||||
@@ -95,7 +97,9 @@ const initializeEditableState = () => {
|
|||||||
editableTerminalFontSize.value = currentTerminalFontSize.value;
|
editableTerminalFontSize.value = currentTerminalFontSize.value;
|
||||||
editableEditorFontSize.value = currentEditorFontSize.value; // <-- 新增
|
editableEditorFontSize.value = currentEditorFontSize.value; // <-- 新增
|
||||||
localTerminalBackgroundEnabled.value = isTerminalBackgroundEnabled.value; // <-- 重新添加:在此处初始化
|
localTerminalBackgroundEnabled.value = isTerminalBackgroundEnabled.value; // <-- 重新添加:在此处初始化
|
||||||
|
editableTerminalBackgroundOverlayOpacity.value = currentTerminalBackgroundOverlayOpacity.value; // 初始化蒙版透明度
|
||||||
console.log(`[StyleCustomizer initializeEditableState] Initializing localTerminalBackgroundEnabled to: ${localTerminalBackgroundEnabled.value} (from store: ${isTerminalBackgroundEnabled.value})`); // 添加日志
|
console.log(`[StyleCustomizer initializeEditableState] Initializing localTerminalBackgroundEnabled to: ${localTerminalBackgroundEnabled.value} (from store: ${isTerminalBackgroundEnabled.value})`); // 添加日志
|
||||||
|
console.log(`[StyleCustomizer initializeEditableState] Initializing editableTerminalBackgroundOverlayOpacity to: ${editableTerminalBackgroundOverlayOpacity.value} (from store: ${currentTerminalBackgroundOverlayOpacity.value})`); // 新增日志
|
||||||
uploadError.value = null;
|
uploadError.value = null;
|
||||||
importError.value = null;
|
importError.value = null;
|
||||||
saveThemeError.value = null;
|
saveThemeError.value = null;
|
||||||
@@ -135,7 +139,9 @@ watch([
|
|||||||
const oldActiveThemeId = oldVals ? oldVals[1] : null;
|
const oldActiveThemeId = oldVals ? oldVals[1] : null;
|
||||||
|
|
||||||
// 仅当非编辑状态时,或活动终端主题ID变化时,或 UI 主题/终端背景启用状态设置本身发生变化时 (例如重置或外部更改),才重新初始化
|
// 仅当非编辑状态时,或活动终端主题ID变化时,或 UI 主题/终端背景启用状态设置本身发生变化时 (例如重置或外部更改),才重新初始化
|
||||||
const settingsChanged = newSettings?.customUiTheme !== oldSettings?.customUiTheme || newSettings?.terminalBackgroundEnabled !== oldSettings?.terminalBackgroundEnabled; // 检查相关设置是否变化
|
const settingsChanged = newSettings?.customUiTheme !== oldSettings?.customUiTheme ||
|
||||||
|
newSettings?.terminalBackgroundEnabled !== oldSettings?.terminalBackgroundEnabled ||
|
||||||
|
newSettings?.terminalBackgroundOverlayOpacity !== oldSettings?.terminalBackgroundOverlayOpacity; // 检查相关设置是否变化
|
||||||
if (!isEditingTheme.value || newActiveThemeId !== oldActiveThemeId || settingsChanged) {
|
if (!isEditingTheme.value || newActiveThemeId !== oldActiveThemeId || settingsChanged) {
|
||||||
console.log(`[StyleCustomizer Watch] Triggering re-initialization. isEditing: ${isEditingTheme.value}, themeIdChanged: ${newActiveThemeId !== oldActiveThemeId}, settingsChanged: ${settingsChanged}`); // 添加日志
|
console.log(`[StyleCustomizer Watch] Triggering re-initialization. isEditing: ${isEditingTheme.value}, themeIdChanged: ${newActiveThemeId !== oldActiveThemeId}, settingsChanged: ${settingsChanged}`); // 添加日志
|
||||||
initializeEditableState(); // 调用修改后的初始化函数
|
initializeEditableState(); // 调用修改后的初始化函数
|
||||||
@@ -150,6 +156,7 @@ watch([
|
|||||||
editableTerminalFontSize.value = newSettings?.terminalFontSize || 14;
|
editableTerminalFontSize.value = newSettings?.terminalFontSize || 14;
|
||||||
editableEditorFontSize.value = newSettings?.editorFontSize || 14; // <-- 新增同步
|
editableEditorFontSize.value = newSettings?.editorFontSize || 14; // <-- 新增同步
|
||||||
// localTerminalBackgroundEnabled.value = newSettings?.terminalBackgroundEnabled ?? true; // <-- 移除此行,避免冲突
|
// localTerminalBackgroundEnabled.value = newSettings?.terminalBackgroundEnabled ?? true; // <-- 移除此行,避免冲突
|
||||||
|
// editableTerminalBackgroundOverlayOpacity.value = newSettings?.terminalBackgroundOverlayOpacity ?? 0.5; // 在 initializeEditableState 中处理
|
||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
@@ -165,6 +172,14 @@ watch(isTerminalBackgroundEnabled, (newValue) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 移除单独监听 isTerminalBackgroundEnabled 的 watcher
|
// 移除单独监听 isTerminalBackgroundEnabled 的 watcher
|
||||||
|
|
||||||
|
// 监听 store 中 currentTerminalBackgroundOverlayOpacity 的变化,以同步本地状态
|
||||||
|
watch(currentTerminalBackgroundOverlayOpacity, (newValue) => {
|
||||||
|
if (editableTerminalBackgroundOverlayOpacity.value !== newValue) {
|
||||||
|
console.log(`[StyleCustomizer Watch currentTerminalBackgroundOverlayOpacity] Store changed to ${newValue}, updating local state.`);
|
||||||
|
editableTerminalBackgroundOverlayOpacity.value = newValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
|
|
||||||
@@ -791,6 +806,22 @@ const handleToggleTerminalBackground = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 保存终端背景蒙版透明度
|
||||||
|
const handleSaveTerminalBackgroundOverlayOpacity = async () => {
|
||||||
|
try {
|
||||||
|
const opacity = Number(editableTerminalBackgroundOverlayOpacity.value);
|
||||||
|
if (isNaN(opacity) || opacity < 0 || opacity > 1) {
|
||||||
|
alert(t('styleCustomizer.errorInvalidOpacityValue')); // 需要添加翻译 "无效的透明度值,必须在0到1之间"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await appearanceStore.setTerminalBackgroundOverlayOpacity(opacity);
|
||||||
|
alert(t('styleCustomizer.terminalBgOverlayOpacitySaved')); // 需要添加翻译 "终端背景蒙版透明度已保存"
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("保存终端背景蒙版透明度失败:", error);
|
||||||
|
alert(t('styleCustomizer.terminalBgOverlayOpacitySaveFailed', { message: error.message })); // 需要添加翻译 "终端背景蒙版透明度保存失败"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Removed handlePageOpacityChange and handleTerminalOpacityChange functions
|
// Removed handlePageOpacityChange and handleTerminalOpacityChange functions
|
||||||
|
|
||||||
// --- 辅助函数 ---
|
// --- 辅助函数 ---
|
||||||
@@ -1188,14 +1219,39 @@ const handleFocusAndSelect = (event: FocusEvent) => {
|
|||||||
|
|
||||||
|
|
||||||
<div v-if="localTerminalBackgroundEnabled">
|
<div v-if="localTerminalBackgroundEnabled">
|
||||||
<div class="w-full h-[100px] md:h-[150px] border border-dashed border-border mb-2 flex justify-center items-center text-text-secondary bg-cover bg-center bg-no-repeat rounded bg-header relative overflow-hidden" :style="{ backgroundImage: terminalBackgroundImage ? `url(${terminalBackgroundImage})` : 'none' }">
|
<div class="w-full h-[100px] md:h-[150px] border border-dashed border-border mb-2 flex justify-center items-center text-text-secondary bg-cover bg-center bg-no-repeat rounded bg-header relative overflow-hidden" :style="{ backgroundImage: terminalBackgroundImage ? `url(${terminalBackgroundImage})` : 'none' }">
|
||||||
<span v-if="!terminalBackgroundImage" class="bg-white/80 px-3 py-1.5 rounded text-sm font-medium text-foreground shadow-sm">{{ t('styleCustomizer.noBackground') }}</span>
|
<!-- 实时预览蒙版 -->
|
||||||
|
<div
|
||||||
|
v-if="terminalBackgroundImage"
|
||||||
|
class="absolute inset-0"
|
||||||
|
:style="{ backgroundColor: `rgba(0, 0, 0, ${editableTerminalBackgroundOverlayOpacity})` }"
|
||||||
|
></div>
|
||||||
|
<span v-if="!terminalBackgroundImage" class="bg-white/80 px-3 py-1.5 rounded text-sm font-medium text-foreground shadow-sm relative z-10">{{ t('styleCustomizer.noBackground') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 mb-4 flex-wrap items-center">
|
<div class="flex gap-2 mb-4 flex-wrap items-center">
|
||||||
<button @click="handleTriggerTerminalBgUpload" class="px-3 py-1.5 text-sm border border-border rounded bg-header hover:bg-border transition duration-200 ease-in-out whitespace-nowrap disabled:opacity-60 disabled:cursor-not-allowed">{{ t('styleCustomizer.uploadTerminalBg') }}</button>
|
<button @click="handleTriggerTerminalBgUpload" class="px-3 py-1.5 text-sm border border-border rounded bg-header hover:bg-border transition duration-200 ease-in-out whitespace-nowrap disabled:opacity-60 disabled:cursor-not-allowed">{{ t('styleCustomizer.uploadTerminalBg') }}</button>
|
||||||
<button @click="handleRemoveTerminalBg" :disabled="!terminalBackgroundImage" class="px-3 py-1.5 text-sm border rounded transition duration-200 ease-in-out whitespace-nowrap bg-error/10 text-error border-error/30 hover:bg-error/20 disabled:opacity-60 disabled:cursor-not-allowed">{{ t('styleCustomizer.removeTerminalBg') }}</button>
|
<button @click="handleRemoveTerminalBg" :disabled="!terminalBackgroundImage" class="px-3 py-1.5 text-sm border rounded transition duration-200 ease-in-out whitespace-nowrap bg-error/10 text-error border-error/30 hover:bg-error/20 disabled:opacity-60 disabled:cursor-not-allowed">{{ t('styleCustomizer.removeTerminalBg') }}</button>
|
||||||
<input type="file" ref="terminalBgFileInput" @change="handleTerminalBgUpload" accept="image/*" class="hidden" />
|
<input type="file" ref="terminalBgFileInput" @change="handleTerminalBgUpload" accept="image/*" class="hidden" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 终端背景蒙版透明度控制 -->
|
||||||
|
<div class="mt-4 pt-4 border-t border-border/50">
|
||||||
|
<label for="terminalBgOverlayOpacity" class="block text-sm font-medium text-foreground mb-1">{{ t('styleCustomizer.terminalBgOverlayOpacity', '终端背景蒙版透明度:') }}</label>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="terminalBgOverlayOpacity"
|
||||||
|
v-model.number="editableTerminalBackgroundOverlayOpacity"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
class="w-full cursor-pointer accent-primary"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-foreground min-w-[3em] text-right">{{ editableTerminalBackgroundOverlayOpacity.toFixed(2) }}</span>
|
||||||
|
<button @click="handleSaveTerminalBackgroundOverlayOpacity" class="px-3 py-1.5 text-sm border border-border rounded bg-header hover:bg-border transition duration-200 ease-in-out whitespace-nowrap">{{ t('common.save') }}</button>
|
||||||
|
</div>
|
||||||
|
<!-- <p class="text-xs text-text-secondary mt-1">{{ t('styleCustomizer.terminalBgOverlayOpacityDesc', '控制背景图片上方黑色蒙版的透明度。0为完全透明,1为完全不透明。') }}</p> -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="p-4 text-center text-text-secondary italic border border-dashed border-border/50 rounded-md">
|
<div v-else class="p-4 text-center text-text-secondary italic border border-dashed border-border/50 rounded-md">
|
||||||
{{ t('styleCustomizer.terminalBgDisabled', '终端背景功能已禁用。') }}
|
{{ t('styleCustomizer.terminalBgDisabled', '终端背景功能已禁用。') }}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
|
const emitWorkspaceEvent = useWorkspaceEventEmitter(); // +++ 获取事件发射器 +++
|
||||||
|
|
||||||
const terminalRef = ref<HTMLElement | null>(null); // 终端容器的引用
|
const terminalRef = ref<HTMLElement | null>(null); // xterm 挂载点的引用 (内部容器)
|
||||||
|
const terminalOuterWrapperRef = ref<HTMLElement | null>(null); // 最外层容器的引用,用于背景图
|
||||||
let terminal: Terminal | null = null;
|
let terminal: Terminal | null = null;
|
||||||
let fitAddon: FitAddon | null = null;
|
let fitAddon: FitAddon | null = null;
|
||||||
let searchAddon: SearchAddon | null = null; // *** 添加 searchAddon 变量 ***
|
let searchAddon: SearchAddon | null = null; // *** 添加 searchAddon 变量 ***
|
||||||
@@ -40,7 +41,8 @@ const {
|
|||||||
currentTerminalFontFamily,
|
currentTerminalFontFamily,
|
||||||
terminalBackgroundImage,
|
terminalBackgroundImage,
|
||||||
currentTerminalFontSize,
|
currentTerminalFontSize,
|
||||||
isTerminalBackgroundEnabled
|
isTerminalBackgroundEnabled,
|
||||||
|
currentTerminalBackgroundOverlayOpacity, // 获取蒙版透明度
|
||||||
} = storeToRefs(appearanceStore);
|
} = storeToRefs(appearanceStore);
|
||||||
|
|
||||||
// --- Settings Store ---
|
// --- Settings Store ---
|
||||||
@@ -85,7 +87,8 @@ const debouncedEmitResize = debounce((term: Terminal) => {
|
|||||||
|
|
||||||
// 立即执行 Fit 并发送 Resize 的函数
|
// 立即执行 Fit 并发送 Resize 的函数
|
||||||
const fitAndEmitResizeNow = (term: Terminal) => {
|
const fitAndEmitResizeNow = (term: Terminal) => {
|
||||||
if (!term || !terminalRef.value) return; // 添加 terminalRef.value 检查
|
// terminalRef 现在指向内部容器,检查它即可
|
||||||
|
if (!term || !terminalRef.value) return;
|
||||||
try {
|
try {
|
||||||
// 确保容器可见且有尺寸
|
// 确保容器可见且有尺寸
|
||||||
if (terminalRef.value.offsetHeight > 0 && terminalRef.value.offsetWidth > 0) {
|
if (terminalRef.value.offsetHeight > 0 && terminalRef.value.offsetWidth > 0) {
|
||||||
@@ -98,6 +101,7 @@ const fitAndEmitResizeNow = (term: Terminal) => {
|
|||||||
// 使用 nextTick 确保 fit() 的效果已反映,再触发 resize
|
// 使用 nextTick 确保 fit() 的效果已反映,再触发 resize
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 再次检查终端实例是否仍然存在
|
// 再次检查终端实例是否仍然存在
|
||||||
|
// terminalRef 现在指向内部容器
|
||||||
if (terminal && terminalRef.value) {
|
if (terminal && terminalRef.value) {
|
||||||
console.log(`[Terminal ${props.sessionId}] Triggering window resize event after immediate fit.`);
|
console.log(`[Terminal ${props.sessionId}] Triggering window resize event after immediate fit.`);
|
||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
@@ -160,6 +164,7 @@ const removeContextMenuListener = () => {
|
|||||||
|
|
||||||
// 初始化终端
|
// 初始化终端
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// xterm 挂载到 terminalRef (内部容器)
|
||||||
if (terminalRef.value) {
|
if (terminalRef.value) {
|
||||||
terminal = new Terminal({
|
terminal = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
@@ -198,8 +203,9 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 监听终端大小变化 (通过 ResizeObserver) - 主要处理浏览器窗口大小变化等
|
// 监听终端大小变化 (通过 ResizeObserver) - 主要处理浏览器窗口大小变化等
|
||||||
|
// ResizeObserver 观察内部容器 terminalRef
|
||||||
if (terminalRef.value) {
|
if (terminalRef.value) {
|
||||||
observedElement = terminalRef.value; // +++ Store the element to be observed +++
|
observedElement = terminalRef.value;
|
||||||
resizeObserver = new ResizeObserver((entries) => {
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
// Only process if the terminal is active
|
// Only process if the terminal is active
|
||||||
if (!props.isActive || !terminal) return;
|
if (!props.isActive || !terminal) return;
|
||||||
@@ -242,6 +248,7 @@ onMounted(() => {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Re-check if still active and terminal exists
|
// Re-check if still active and terminal exists
|
||||||
|
// 检查内部容器 terminalRef
|
||||||
if (props.isActive && terminal && terminalRef.value && terminalRef.value.offsetHeight > 0) {
|
if (props.isActive && terminal && terminalRef.value && terminalRef.value.offsetHeight > 0) {
|
||||||
fitAndEmitResizeNow(terminal);
|
fitAndEmitResizeNow(terminal);
|
||||||
// Also ensure focus when becoming active
|
// Also ensure focus when becoming active
|
||||||
@@ -445,7 +452,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 重新添加鼠标滚轮缩放功能
|
// 重新添加鼠标滚轮缩放功能到内部容器 terminalRef
|
||||||
if (terminalRef.value) {
|
if (terminalRef.value) {
|
||||||
terminalRef.value.addEventListener('wheel', (event: WheelEvent) => {
|
terminalRef.value.addEventListener('wheel', (event: WheelEvent) => {
|
||||||
// // 只在按下Ctrl键时才触发缩放
|
// // 只在按下Ctrl键时才触发缩放
|
||||||
@@ -534,9 +541,9 @@ onBeforeUnmount(() => {
|
|||||||
// 确保在卸载时移除右键监听器
|
// 确保在卸载时移除右键监听器
|
||||||
removeContextMenuListener();
|
removeContextMenuListener();
|
||||||
|
|
||||||
if (terminalRef.value) {
|
// terminalRef 是内部容器,不需要特别处理
|
||||||
|
// if (terminalRef.value) {
|
||||||
}
|
// }
|
||||||
});
|
});
|
||||||
// 暴露 write 方法给父组件 (可选)
|
// 暴露 write 方法给父组件 (可选)
|
||||||
const write = (data: string | Uint8Array) => {
|
const write = (data: string | Uint8Array) => {
|
||||||
@@ -572,60 +579,40 @@ defineExpose({ write, findNext, findPrevious, clearSearch, clear }); // 暴露 c
|
|||||||
|
|
||||||
// --- 应用终端背景 ---
|
// --- 应用终端背景 ---
|
||||||
const applyTerminalBackground = () => {
|
const applyTerminalBackground = () => {
|
||||||
if (terminalRef.value) {
|
// 背景应用到 terminalOuterWrapperRef
|
||||||
// 首先检查背景功能是否启用
|
if (terminalOuterWrapperRef.value) {
|
||||||
if (!isTerminalBackgroundEnabled.value) {
|
if (!isTerminalBackgroundEnabled.value) {
|
||||||
// 如果禁用,则移除背景并返回
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (terminalRef.value) {
|
if (terminalOuterWrapperRef.value) {
|
||||||
terminalRef.value.style.backgroundImage = 'none';
|
terminalOuterWrapperRef.value.style.backgroundImage = 'none';
|
||||||
terminalRef.value.classList.remove('has-terminal-background');
|
terminalOuterWrapperRef.value.classList.remove('has-terminal-background');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log(`[Terminal ${props.sessionId}] 终端背景已禁用,移除背景。`);
|
console.log(`[Terminal ${props.sessionId}] 终端背景已禁用,移除背景。`);
|
||||||
return; // 提前退出
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果启用,再检查是否有背景图片
|
|
||||||
if (terminalBackgroundImage.value) {
|
if (terminalBackgroundImage.value) {
|
||||||
// --- 修改开始 ---
|
const backendUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||||
// 使用环境变量获取后端基础 URL
|
|
||||||
const backendUrl = import.meta.env.VITE_API_BASE_URL || ''; // 提供一个默认空字符串以防万一
|
|
||||||
const imagePath = terminalBackgroundImage.value;
|
const imagePath = terminalBackgroundImage.value;
|
||||||
console.log(`[Terminal applyTerminalBackground] backendUrl: "${backendUrl}", imagePath: "${imagePath}"`); // 详细日志
|
|
||||||
const fullImageUrl = `${backendUrl}${imagePath}`;
|
const fullImageUrl = `${backendUrl}${imagePath}`;
|
||||||
console.log(`[Terminal applyTerminalBackground] fullImageUrl: "${fullImageUrl}"`); // 打印完整 URL
|
|
||||||
// --- 修改结束 ---
|
|
||||||
// --- 使用 nextTick 包装样式应用 ---
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (terminalRef.value) { // 再次检查 ref 是否存在
|
if (terminalOuterWrapperRef.value) {
|
||||||
terminalRef.value.style.backgroundImage = `url(${fullImageUrl})`;
|
terminalOuterWrapperRef.value.style.backgroundImage = `url(${fullImageUrl})`;
|
||||||
terminalRef.value.style.backgroundSize = 'cover'; // Or 'contain', 'auto', etc.
|
terminalOuterWrapperRef.value.style.backgroundSize = 'cover';
|
||||||
terminalRef.value.style.backgroundPosition = 'center';
|
terminalOuterWrapperRef.value.style.backgroundPosition = 'center';
|
||||||
terminalRef.value.style.backgroundRepeat = 'no-repeat';
|
terminalOuterWrapperRef.value.style.backgroundRepeat = 'no-repeat';
|
||||||
// 添加 CSS 类
|
terminalOuterWrapperRef.value.classList.add('has-terminal-background');
|
||||||
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}`);
|
console.log(`[Terminal ${props.sessionId}] 应用终端背景图片: ${terminalBackgroundImage.value}`);
|
||||||
} else {
|
} else {
|
||||||
// --- 使用 nextTick 包装样式移除 ---
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (terminalRef.value) { // 再次检查 ref 是否存在
|
if (terminalOuterWrapperRef.value) {
|
||||||
terminalRef.value.style.backgroundImage = 'none';
|
terminalOuterWrapperRef.value.style.backgroundImage = 'none';
|
||||||
// 移除 CSS 类
|
terminalOuterWrapperRef.value.classList.remove('has-terminal-background');
|
||||||
terminalRef.value.classList.remove('has-terminal-background');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// terminalRef.value.style.opacity = '1'; // 移除背景时恢复不透明
|
|
||||||
console.log(`[Terminal ${props.sessionId}] 移除终端背景图片。`);
|
console.log(`[Terminal ${props.sessionId}] 移除终端背景图片。`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,41 +621,46 @@ const applyTerminalBackground = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="terminalRef" class="terminal-container"></div>
|
<div ref="terminalOuterWrapperRef" class="terminal-outer-wrapper">
|
||||||
|
<!-- 蒙版层 -->
|
||||||
|
<div
|
||||||
|
v-if="isTerminalBackgroundEnabled && terminalBackgroundImage"
|
||||||
|
class="terminal-background-overlay"
|
||||||
|
:style="{ backgroundColor: `rgba(0, 0, 0, ${currentTerminalBackgroundOverlayOpacity})` }"
|
||||||
|
></div>
|
||||||
|
<!-- xterm 实际挂载点 -->
|
||||||
|
<div ref="terminalRef" class="terminal-inner-container"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 容器样式,确保填满并隐藏自身滚动条 */
|
.terminal-outer-wrapper {
|
||||||
.terminal-container {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%; /* 高度需要由父容器控制 */
|
height: 100%;
|
||||||
overflow: hidden; /* 阻止此容器本身产生滚动条 */
|
overflow: hidden;
|
||||||
position: relative; /* 用于可能的伪元素背景 */
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移除 :deep 样式,让 xterm 内部自然处理滚动 */
|
.terminal-background-overlay {
|
||||||
|
|
||||||
/* 示例:使用伪元素添加带透明度的背景层 (如果需要独立于主题的透明度) */
|
|
||||||
/*
|
|
||||||
.terminal-container::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
width: 100%;
|
||||||
bottom: 0;
|
height: 100%;
|
||||||
background-image: var(--terminal-bg-image);
|
pointer-events: none; /* 允许鼠标事件穿透 */
|
||||||
background-size: cover;
|
z-index: 1; /* 在背景图之上 */
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
opacity: var(--terminal-bg-opacity);
|
|
||||||
z-index: -1; // 确保在 xterm 内容后面
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/* 当容器有背景图时,强制内部 xterm 视口和屏幕背景透明 */
|
.terminal-inner-container {
|
||||||
.terminal-container.has-terminal-background :deep(.xterm-viewport),
|
width: 100%;
|
||||||
.terminal-container.has-terminal-background :deep(.xterm-screen) {
|
height: 100%;
|
||||||
|
position: relative; /* 使 z-index 生效 */
|
||||||
|
z-index: 2; /* 在蒙版之上 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当最外层容器有背景图时,强制内部 xterm 视口和屏幕背景透明 */
|
||||||
|
.terminal-outer-wrapper.has-terminal-background .terminal-inner-container :deep(.xterm-viewport),
|
||||||
|
.terminal-outer-wrapper.has-terminal-background .terminal-inner-container :deep(.xterm-screen) {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -91,7 +91,12 @@
|
|||||||
"defaultMode": "Default Mode",
|
"defaultMode": "Default Mode",
|
||||||
"darkMode": "Dark Mode",
|
"darkMode": "Dark Mode",
|
||||||
"darkModeApplied": "Dark mode applied",
|
"darkModeApplied": "Dark mode applied",
|
||||||
"darkModeApplyFailed": "Failed to apply dark mode: {message}"
|
"darkModeApplyFailed": "Failed to apply dark mode: {message}",
|
||||||
|
"terminalBgOverlayOpacity": "Terminal Background Overlay Opacity:",
|
||||||
|
"terminalBgOverlayOpacityDesc": "Controls the opacity of the black overlay on top of the background image. 0 is fully transparent, 1 is fully opaque.",
|
||||||
|
"errorInvalidOpacityValue": "Invalid opacity value. Must be between 0 and 1.",
|
||||||
|
"terminalBgOverlayOpacitySaved": "Terminal background overlay opacity saved.",
|
||||||
|
"terminalBgOverlayOpacitySaveFailed": "Failed to save terminal background overlay opacity: {message}"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "User Login",
|
"title": "User Login",
|
||||||
|
|||||||
@@ -1260,9 +1260,14 @@
|
|||||||
"uiThemeSaveFailed": "UI テーマの保存に失敗しました: {message}",
|
"uiThemeSaveFailed": "UI テーマの保存に失敗しました: {message}",
|
||||||
"uploadFailed": "アップロードに失敗しました: {message}",
|
"uploadFailed": "アップロードに失敗しました: {message}",
|
||||||
"uploadPageBg": "ページの背景をアップロード",
|
"uploadPageBg": "ページの背景をアップロード",
|
||||||
"uploadTerminalBg": "ターミナルの背景をアップロード"
|
"uploadTerminalBg": "ターミナルの背景をアップロード",
|
||||||
},
|
"terminalBgOverlayOpacity": "ターミナル背景オーバーレイの不透明度:",
|
||||||
"tags": {
|
"terminalBgOverlayOpacityDesc": "背景画像上の黒いオーバーレイの不透明度を制御します。0は完全に透明、1は完全に不透明です。",
|
||||||
|
"errorInvalidOpacityValue": "無効な不透明度の値です。0から1の間でなければなりません。",
|
||||||
|
"terminalBgOverlayOpacitySaved": "ターミナル背景オーバーレイの不透明度が保存されました。",
|
||||||
|
"terminalBgOverlayOpacitySaveFailed": "ターミナル背景オーバーレイの不透明度の保存に失敗しました: {message}"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
"addTag": "新しいタグを追加",
|
"addTag": "新しいタグを追加",
|
||||||
"deleteTagGlobally": "このタグをグローバルに削除",
|
"deleteTagGlobally": "このタグをグローバルに削除",
|
||||||
"error": "タグリストのロードに失敗しました: {error}",
|
"error": "タグリストのロードに失敗しました: {error}",
|
||||||
|
|||||||
@@ -90,7 +90,12 @@
|
|||||||
"defaultMode": "默认模式",
|
"defaultMode": "默认模式",
|
||||||
"darkMode": "黑暗模式",
|
"darkMode": "黑暗模式",
|
||||||
"darkModeApplied": "黑暗模式已应用",
|
"darkModeApplied": "黑暗模式已应用",
|
||||||
"darkModeApplyFailed": "应用黑暗模式失败: {message}"
|
"darkModeApplyFailed": "应用黑暗模式失败: {message}",
|
||||||
|
"terminalBgOverlayOpacity": "终端背景蒙版透明度:",
|
||||||
|
"terminalBgOverlayOpacityDesc": "控制背景图片上方黑色蒙版的透明度。0为完全透明,1为完全不透明。",
|
||||||
|
"errorInvalidOpacityValue": "无效的透明度值,必须在0到1之间",
|
||||||
|
"terminalBgOverlayOpacitySaved": "终端背景蒙版透明度已保存",
|
||||||
|
"terminalBgOverlayOpacitySaveFailed": "终端背景蒙版透明度保存失败: {message}"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "用户登录",
|
"title": "用户登录",
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
|||||||
return typeof enabled === 'boolean' ? enabled : true; // 默认启用
|
return typeof enabled === 'boolean' ? enabled : true; // 默认启用
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 终端背景蒙版透明度
|
||||||
|
const currentTerminalBackgroundOverlayOpacity = computed<number>(() => {
|
||||||
|
const opacity = appearanceSettings.value.terminalBackgroundOverlayOpacity;
|
||||||
|
return typeof opacity === 'number' && opacity >= 0 && opacity <= 1 ? opacity : 0.5; // 默认 0.5
|
||||||
|
});
|
||||||
|
|
||||||
// --- Actions ---
|
// --- Actions ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,6 +240,14 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
|||||||
console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 更新后端调用完成。`); // 添加日志
|
console.log(`[AppearanceStore LOG] setTerminalBackgroundEnabled 更新后端调用完成。`); // 添加日志
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置终端背景蒙版透明度
|
||||||
|
* @param opacity 透明度 (0-1)
|
||||||
|
*/
|
||||||
|
async function setTerminalBackgroundOverlayOpacity(opacity: number) {
|
||||||
|
await updateAppearanceSettings({ terminalBackgroundOverlayOpacity: opacity });
|
||||||
|
}
|
||||||
|
|
||||||
// --- 终端主题列表管理 Actions ---
|
// --- 终端主题列表管理 Actions ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -567,6 +581,7 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
|||||||
// pageBackgroundOpacity, // Removed
|
// pageBackgroundOpacity, // Removed
|
||||||
terminalBackgroundImage,
|
terminalBackgroundImage,
|
||||||
// terminalBackgroundOpacity, // Removed
|
// terminalBackgroundOpacity, // Removed
|
||||||
|
currentTerminalBackgroundOverlayOpacity, // <-- 新增导出
|
||||||
// Actions
|
// Actions
|
||||||
loadInitialAppearanceData,
|
loadInitialAppearanceData,
|
||||||
updateAppearanceSettings,
|
updateAppearanceSettings,
|
||||||
@@ -586,6 +601,7 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
|||||||
uploadTerminalBackground,
|
uploadTerminalBackground,
|
||||||
// setPageBackgroundOpacity, // Removed
|
// setPageBackgroundOpacity, // Removed
|
||||||
// setTerminalBackgroundOpacity, // Removed
|
// setTerminalBackgroundOpacity, // Removed
|
||||||
|
setTerminalBackgroundOverlayOpacity, // <-- 新增导出
|
||||||
removePageBackground,
|
removePageBackground,
|
||||||
removeTerminalBackground,
|
removeTerminalBackground,
|
||||||
loadTerminalThemeData, // <-- 新增导出
|
loadTerminalThemeData, // <-- 新增导出
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface AppearanceSettings {
|
|||||||
pageBackgroundImage?: string;
|
pageBackgroundImage?: string;
|
||||||
editorFontSize?: number;
|
editorFontSize?: number;
|
||||||
terminalBackgroundEnabled?: boolean; // 终端背景是否启用
|
terminalBackgroundEnabled?: boolean; // 终端背景是否启用
|
||||||
|
terminalBackgroundOverlayOpacity?: number; // 终端背景蒙版透明度 (0-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 前端用于更新外观设置的数据结构 (对应 API 请求体)
|
// 前端用于更新外观设置的数据结构 (对应 API 请求体)
|
||||||
|
|||||||
Reference in New Issue
Block a user