feat: 添加自定义终端字体描边和阴影设置项

This commit is contained in:
Baobhan Sith
2025-05-27 19:15:52 +08:00
parent 03fd91a7c0
commit c7fd6c3df7
13 changed files with 501 additions and 17 deletions
@@ -49,7 +49,6 @@ const sourceConnectionId = computed(() => { // +++ 获取并转换源服务器 I
return null;
});
// +++ 新增:用于菜单位置调整的 ref +++
const contextMenuRef = ref<HTMLDivElement | null>(null);
const computedRenderPosition = ref({ x: props.position.x, y: props.position.y });
+79 -2
View File
@@ -51,6 +51,15 @@ const {
isTerminalBackgroundEnabled,
currentTerminalBackgroundOverlayOpacity, // 获取蒙版透明度
terminalCustomHTML, // 用于自定义终端背景 HTML
// --- 文字描边和阴影状态 ---
terminalTextStrokeEnabled,
terminalTextStrokeWidth,
terminalTextStrokeColor,
terminalTextShadowEnabled,
terminalTextShadowOffsetX,
terminalTextShadowOffsetY,
terminalTextShadowBlur,
terminalTextShadowColor,
} = storeToRefs(appearanceStore);
// --- Settings Store ---
@@ -610,8 +619,57 @@ const clear = () => {
};
defineExpose({ write, findNext, findPrevious, clearSearch, clear }); // 暴露 clear 方法
// --- 文字描边和阴影 ---
const applyTerminalTextStyles = () => {
if (terminalRef.value && terminal?.element) {
const hostElement = terminalRef.value; // .terminal-inner-container
// 清理类名
hostElement.classList.remove('has-text-stroke', 'has-text-shadow');
// 文字描边
if (terminalTextStrokeEnabled.value) {
hostElement.classList.add('has-text-stroke');
hostElement.style.setProperty('--terminal-stroke-width', `${terminalTextStrokeWidth.value}px`);
hostElement.style.setProperty('--terminal-stroke-color', terminalTextStrokeColor.value);
} else {
hostElement.style.removeProperty('--terminal-stroke-width');
hostElement.style.removeProperty('--terminal-stroke-color');
}
// 文字阴影
if (terminalTextShadowEnabled.value) {
hostElement.classList.add('has-text-shadow');
const shadowValue = `${terminalTextShadowOffsetX.value}px ${terminalTextShadowOffsetY.value}px ${terminalTextShadowBlur.value}px ${terminalTextShadowColor.value}`;
hostElement.style.setProperty('--terminal-shadow', shadowValue);
} else {
hostElement.style.removeProperty('--terminal-shadow');
}
// console.log('[Terminal] Applied text styles. Stroke enabled:', terminalTextStrokeEnabled.value, 'Shadow enabled:', terminalTextShadowEnabled.value);
}
};
// 监听文字描边和阴影设置的变化
watch(
[
terminalTextStrokeEnabled,
terminalTextStrokeWidth,
terminalTextStrokeColor,
terminalTextShadowEnabled,
terminalTextShadowOffsetX,
terminalTextShadowOffsetY,
terminalTextShadowBlur,
terminalTextShadowColor,
],
() => {
// console.log('[Terminal] Text style settings changed, applying new styles.');
applyTerminalTextStyles();
},
{ deep: true, immediate: true } // immediate: true 确保挂载时应用初始设置
);
// --- 应用终端背景 ---
const applyTerminalBackground = () => {
// 背景应用到 terminalOuterWrapperRef
@@ -751,6 +809,25 @@ watch(terminalCustomHTML, (newHtmlContent) => {
z-index: 2; /* 在蒙版之上 */
}
/* 文字描边和阴影样式 */
.terminal-inner-container.has-text-stroke :deep(.xterm-rows span),
.terminal-inner-container.has-text-stroke :deep(.xterm-rows div > span), /* 更具体地针对嵌套 span */
.terminal-inner-container.has-text-stroke :deep(.xterm-rows div) { /* 针对直接包含文本的 div */
-webkit-text-stroke-width: var(--terminal-stroke-width);
-webkit-text-stroke-color: var(--terminal-stroke-color);
text-stroke-width: var(--terminal-stroke-width);
text-stroke-color: var(--terminal-stroke-color);
/* 确保描边在填充之下,这样填充色仍然可见 */
paint-order: stroke fill;
-webkit-paint-order: stroke fill; /* 兼容 WebKit */
}
.terminal-inner-container.has-text-shadow :deep(.xterm-rows span),
.terminal-inner-container.has-text-shadow :deep(.xterm-rows div > span),
.terminal-inner-container.has-text-shadow :deep(.xterm-rows div) {
text-shadow: var(--terminal-shadow);
}
/* 当最外层容器有背景图时,强制内部 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) {
@@ -45,7 +45,6 @@ const formatTaskTitle = (task: TransferTask): string => {
return `${sourceServerName} (${fileName} -> ${targetPath})`;
};
// --- 新增:文件传输相关 ---
// 数据结构参考
interface TransferSubTask {
@@ -28,10 +28,28 @@ const {
activeTerminalThemeId,
currentTerminalFontFamily,
currentTerminalFontSize,
terminalTextStrokeEnabled,
terminalTextStrokeWidth,
terminalTextStrokeColor,
terminalTextShadowEnabled,
terminalTextShadowOffsetX,
terminalTextShadowOffsetY,
terminalTextShadowBlur,
terminalTextShadowColor,
} = storeToRefs(appearanceStore);
const editableTerminalFontFamily = ref('');
const editableTerminalFontSize = ref(14);
const editableTerminalTextStrokeEnabled = ref(false);
const editableTerminalTextStrokeWidth = ref(1);
const editableTerminalTextStrokeColor = ref('#000000');
const editableTerminalTextShadowEnabled = ref(false);
const editableTerminalTextShadowOffsetX = ref(0);
const editableTerminalTextShadowOffsetY = ref(0);
const editableTerminalTextShadowBlur = ref(0);
const editableTerminalTextShadowColor = ref('rgba(0,0,0,0.5)');
const themeSearchTerm = ref('');
const saveThemeError = ref<string | null>(null);
const editableTerminalThemeString = ref('');
@@ -56,11 +74,21 @@ brightBlue: #5555ff
brightMagenta: #ff55ff
brightCyan: #55ffff
brightWhite: #ffffff`;
// Theme preview refs
const initializeEditableState = () => {
editableTerminalFontFamily.value = currentTerminalFontFamily.value;
editableTerminalFontSize.value = currentTerminalFontSize.value;
editableTerminalTextStrokeEnabled.value = terminalTextStrokeEnabled.value;
editableTerminalTextStrokeWidth.value = terminalTextStrokeWidth.value;
editableTerminalTextStrokeColor.value = terminalTextStrokeColor.value;
editableTerminalTextShadowEnabled.value = terminalTextShadowEnabled.value;
editableTerminalTextShadowOffsetX.value = terminalTextShadowOffsetX.value;
editableTerminalTextShadowOffsetY.value = terminalTextShadowOffsetY.value;
editableTerminalTextShadowBlur.value = terminalTextShadowBlur.value;
editableTerminalTextShadowColor.value = terminalTextShadowColor.value;
saveThemeError.value = null;
terminalThemeParseError.value = null;
};
@@ -79,13 +107,27 @@ watch(currentTerminalFontSize, (newValue) => {
});
// Initialize on mount and when relevant props change
watch(() => [currentTerminalFontFamily.value, currentTerminalFontSize.value], () => {
// Re-initialize only if not in the middle of editing a theme,
// as editing a theme might involve temporary font changes or different contexts.
watch(
() => [
currentTerminalFontFamily.value,
currentTerminalFontSize.value,
terminalTextStrokeEnabled.value,
terminalTextStrokeWidth.value,
terminalTextStrokeColor.value,
terminalTextShadowEnabled.value,
terminalTextShadowOffsetX.value,
terminalTextShadowOffsetY.value,
terminalTextShadowBlur.value,
terminalTextShadowColor.value,
],
() => {
// Re-initialize only if not in the middle of editing a theme
if (!props.isEditingTheme) {
initializeEditableState();
initializeEditableState();
}
}, { immediate: true });
},
{ immediate: true, deep: true }
);
// Methods
@@ -114,6 +156,32 @@ const handleSaveTerminalFontSize = async () => {
}
};
const handleSaveTerminalTextStroke = async () => {
try {
await appearanceStore.setTerminalTextStrokeEnabled(editableTerminalTextStrokeEnabled.value);
await appearanceStore.setTerminalTextStrokeWidth(Number(editableTerminalTextStrokeWidth.value));
await appearanceStore.setTerminalTextStrokeColor(editableTerminalTextStrokeColor.value);
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.textStrokeSettingsSaved') });
} catch (error: any) {
console.error("保存文字描边设置失败:", error);
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.textStrokeSettingsSaveFailed', { message: error.message }) });
}
};
const handleSaveTerminalTextShadow = async () => {
try {
await appearanceStore.setTerminalTextShadowEnabled(editableTerminalTextShadowEnabled.value);
await appearanceStore.setTerminalTextShadowOffsetX(Number(editableTerminalTextShadowOffsetX.value));
await appearanceStore.setTerminalTextShadowOffsetY(Number(editableTerminalTextShadowOffsetY.value));
await appearanceStore.setTerminalTextShadowBlur(Number(editableTerminalTextShadowBlur.value));
await appearanceStore.setTerminalTextShadowColor(editableTerminalTextShadowColor.value);
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.textShadowSettingsSaved') });
} catch (error: any) {
console.error("保存文字阴影设置失败:", error);
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.textShadowSettingsSaveFailed', { message: error.message }) });
}
};
const handleApplyTheme = async (theme: TerminalTheme) => {
if (!theme._id) return;
const themeIdNum = parseInt(theme._id, 10);
@@ -425,7 +493,69 @@ watch(() => props.isEditingTheme, (isEditing) => {
<button @click="handleSaveTerminalFontSize" 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 justify-self-start mt-1 md:mt-0">{{ t('common.save') }}</button>
</div>
<hr class="my-4 md:my-6">
<!-- 文字描边设置 -->
<hr class="my-4 md:my-6">
<h4 class="mt-6 mb-3 text-base font-semibold text-foreground">{{ t('styleCustomizer.textStrokeSettings') }}</h4>
<div class="space-y-3 mb-3">
<div class="flex items-center gap-2">
<input type="checkbox" id="terminalTextStrokeEnabled" v-model="editableTerminalTextStrokeEnabled" class="h-4 w-4 rounded border-border text-primary focus:ring-primary cursor-pointer">
<label for="terminalTextStrokeEnabled" class="text-foreground text-sm font-medium cursor-pointer">{{ t('styleCustomizer.enableTextStroke') }}</label>
</div>
<div class="space-y-3">
<div class="grid grid-cols-1 md:grid-cols-[auto_1fr] items-center gap-2">
<label for="terminalTextStrokeWidth" class="text-left text-foreground text-sm font-medium">{{ t('styleCustomizer.textStrokeWidth') }}:</label>
<input type="number" id="terminalTextStrokeWidth" v-model.number="editableTerminalTextStrokeWidth" min="0" step="0.1" class="border border-border px-[0.7rem] py-2 rounded text-sm bg-background text-foreground max-w-[100px] justify-self-start box-border">
</div>
<div class="grid grid-cols-1 md:grid-cols-[auto_1fr] items-center gap-2">
<label for="terminalTextStrokeColor" class="text-left text-foreground text-sm font-medium">{{ t('styleCustomizer.textStrokeColor') }}:</label>
<div class="flex items-center gap-2">
<input type="color" id="terminalTextStrokeColor" v-model="editableTerminalTextStrokeColor" class="p-0.5 h-[34px] min-w-[40px] max-w-[50px] rounded border border-border flex-shrink-0">
<input type="text" :value="editableTerminalTextStrokeColor" @input="editableTerminalTextStrokeColor = ($event.target as HTMLInputElement).value" class="flex-grow min-w-[80px] bg-header border border-border px-[0.7rem] py-2 rounded text-sm text-foreground box-border">
</div>
</div>
</div>
<div class="flex justify-start mt-2">
<button @click="handleSaveTerminalTextStroke" 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>
</div>
<!-- 文字阴影设置 -->
<hr class="my-4 md:my-6">
<h4 class="mt-6 mb-3 text-base font-semibold text-foreground">{{ t('styleCustomizer.textShadowSettings') }}</h4>
<div class="space-y-3 mb-3">
<div class="flex items-center gap-2">
<input type="checkbox" id="terminalTextShadowEnabled" v-model="editableTerminalTextShadowEnabled" class="h-4 w-4 rounded border-border text-primary focus:ring-primary cursor-pointer">
<label for="terminalTextShadowEnabled" class="text-foreground text-sm font-medium cursor-pointer">{{ t('styleCustomizer.enableTextShadow') }}</label>
</div>
<div class="space-y-3">
<div class="grid grid-cols-1 md:grid-cols-[auto_1fr] items-center gap-2">
<label for="terminalTextShadowOffsetX" class="text-left text-foreground text-sm font-medium">{{ t('styleCustomizer.textShadowOffsetX') }}:</label>
<input type="number" id="terminalTextShadowOffsetX" v-model.number="editableTerminalTextShadowOffsetX" step="0.1" class="border border-border px-[0.7rem] py-2 rounded text-sm bg-background text-foreground max-w-[100px] justify-self-start box-border">
</div>
<div class="grid grid-cols-1 md:grid-cols-[auto_1fr] items-center gap-2">
<label for="terminalTextShadowOffsetY" class="text-left text-foreground text-sm font-medium">{{ t('styleCustomizer.textShadowOffsetY') }}:</label>
<input type="number" id="terminalTextShadowOffsetY" v-model.number="editableTerminalTextShadowOffsetY" step="0.1" class="border border-border px-[0.7rem] py-2 rounded text-sm bg-background text-foreground max-w-[100px] justify-self-start box-border">
</div>
<div class="grid grid-cols-1 md:grid-cols-[auto_1fr] items-center gap-2">
<label for="terminalTextShadowBlur" class="text-left text-foreground text-sm font-medium">{{ t('styleCustomizer.textShadowBlur') }}:</label>
<input type="number" id="terminalTextShadowBlur" v-model.number="editableTerminalTextShadowBlur" min="0" step="0.1" class="border border-border px-[0.7rem] py-2 rounded text-sm bg-background text-foreground max-w-[100px] justify-self-start box-border">
</div>
<div class="grid grid-cols-1 md:grid-cols-[auto_1fr] items-center gap-2">
<label for="terminalTextShadowColor" class="text-left text-foreground text-sm font-medium">{{ t('styleCustomizer.textShadowColor') }}:</label>
<div class="flex items-center gap-2">
<input type="color" id="terminalTextShadowColor" v-model="editableTerminalTextShadowColor" class="p-0.5 h-[34px] min-w-[40px] max-w-[50px] rounded border border-border flex-shrink-0">
<input type="text" :value="editableTerminalTextShadowColor" @input="editableTerminalTextShadowColor = ($event.target as HTMLInputElement).value" class="flex-grow min-w-[80px] bg-header border border-border px-[0.7rem] py-2 rounded text-sm text-foreground box-border">
</div>
</div>
</div>
<div class="flex justify-start mt-2">
<button @click="handleSaveTerminalTextShadow" 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>
</div>
<hr class="my-4 md:my-6">
<h4 class="mt-6 mb-2 text-base font-semibold text-foreground">{{ t('styleCustomizer.terminalThemeSelection') }}</h4>