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
@@ -20,6 +20,14 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
let latestUpdatedAt = 0;
let terminalBackgroundEnabledFound = false; // 标记是否在数据库中找到该设置
let terminalBackgroundOverlayOpacityFound = false; // 标记是否找到蒙版透明度设置
let terminalTextStrokeEnabledFound = false;
let terminalTextStrokeWidthFound = false;
let terminalTextStrokeColorFound = false;
let terminalTextShadowEnabledFound = false;
let terminalTextShadowOffsetXFound = false;
let terminalTextShadowOffsetYFound = false;
let terminalTextShadowBlurFound = false;
let terminalTextShadowColorFound = false;
for (const row of rows) {
// 更新 latestUpdatedAt
@@ -67,6 +75,38 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
case 'remote_html_presets_url':
settings.remoteHtmlPresetsUrl = row.value || null; // 如果为空字符串,则视为 null
break;
case 'terminalTextStrokeEnabled':
settings.terminalTextStrokeEnabled = row.value === 'true';
terminalTextStrokeEnabledFound = true;
break;
case 'terminalTextStrokeWidth':
settings.terminalTextStrokeWidth = parseFloat(row.value);
terminalTextStrokeWidthFound = true;
break;
case 'terminalTextStrokeColor':
settings.terminalTextStrokeColor = row.value;
terminalTextStrokeColorFound = true;
break;
case 'terminalTextShadowEnabled':
settings.terminalTextShadowEnabled = row.value === 'true';
terminalTextShadowEnabledFound = true;
break;
case 'terminalTextShadowOffsetX':
settings.terminalTextShadowOffsetX = parseFloat(row.value);
terminalTextShadowOffsetXFound = true;
break;
case 'terminalTextShadowOffsetY':
settings.terminalTextShadowOffsetY = parseFloat(row.value);
terminalTextShadowOffsetYFound = true;
break;
case 'terminalTextShadowBlur':
settings.terminalTextShadowBlur = parseFloat(row.value);
terminalTextShadowBlurFound = true;
break;
case 'terminalTextShadowColor':
settings.terminalTextShadowColor = row.value;
terminalTextShadowColorFound = true;
break;
}
}
@@ -90,6 +130,30 @@ const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): Appearanc
: defaults.terminalBackgroundOverlayOpacity, // 否则使用默认值
terminal_custom_html: settings.terminal_custom_html ?? defaults.terminal_custom_html,
remoteHtmlPresetsUrl: settings.remoteHtmlPresetsUrl ?? defaults.remoteHtmlPresetsUrl,
terminalTextStrokeEnabled: terminalTextStrokeEnabledFound
? settings.terminalTextStrokeEnabled
: defaults.terminalTextStrokeEnabled,
terminalTextStrokeWidth: terminalTextStrokeWidthFound
? settings.terminalTextStrokeWidth
: defaults.terminalTextStrokeWidth,
terminalTextStrokeColor: terminalTextStrokeColorFound
? settings.terminalTextStrokeColor
: defaults.terminalTextStrokeColor,
terminalTextShadowEnabled: terminalTextShadowEnabledFound
? settings.terminalTextShadowEnabled
: defaults.terminalTextShadowEnabled,
terminalTextShadowOffsetX: terminalTextShadowOffsetXFound
? settings.terminalTextShadowOffsetX
: defaults.terminalTextShadowOffsetX,
terminalTextShadowOffsetY: terminalTextShadowOffsetYFound
? settings.terminalTextShadowOffsetY
: defaults.terminalTextShadowOffsetY,
terminalTextShadowBlur: terminalTextShadowBlurFound
? settings.terminalTextShadowBlur
: defaults.terminalTextShadowBlur,
terminalTextShadowColor: terminalTextShadowColorFound
? settings.terminalTextShadowColor
: defaults.terminalTextShadowColor,
updatedAt: latestUpdatedAt || defaults.updatedAt, // 使用最新的更新时间,否则使用默认时间戳
};
};
@@ -110,6 +174,17 @@ const getDefaultAppearanceSettings = (): Omit<AppearanceSettings, '_id'> => {
terminalBackgroundOverlayOpacity: 0.5, // 默认蒙版透明度
terminal_custom_html: '', // 默认自定义 HTML 为空字符串
remoteHtmlPresetsUrl: null, // 默认远程 HTML 预设 URL 为 null
// 终端文本描边设置默认值
terminalTextStrokeEnabled: false,
terminalTextStrokeWidth: 1,
terminalTextStrokeColor: '#000000',
// 终端文本阴影设置默认值
terminalTextShadowEnabled: false,
terminalTextShadowOffsetX: 2,
terminalTextShadowOffsetY: 2,
terminalTextShadowBlur: 0,
terminalTextShadowColor: '#000000',
updatedAt: Date.now(), // 提供默认时间戳
};
};
@@ -139,6 +214,14 @@ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise<
{ key: 'terminalBackgroundOverlayOpacity', value: defaults.terminalBackgroundOverlayOpacity },
{ key: 'terminal_custom_html', value: defaults.terminal_custom_html },
{ key: 'remoteHtmlPresetsUrl', value: defaults.remoteHtmlPresetsUrl },
{ key: 'terminalTextStrokeEnabled', value: defaults.terminalTextStrokeEnabled },
{ key: 'terminalTextStrokeWidth', value: defaults.terminalTextStrokeWidth },
{ key: 'terminalTextStrokeColor', value: defaults.terminalTextStrokeColor },
{ key: 'terminalTextShadowEnabled', value: defaults.terminalTextShadowEnabled },
{ key: 'terminalTextShadowOffsetX', value: defaults.terminalTextShadowOffsetX },
{ key: 'terminalTextShadowOffsetY', value: defaults.terminalTextShadowOffsetY },
{ key: 'terminalTextShadowBlur', value: defaults.terminalTextShadowBlur },
{ key: 'terminalTextShadowColor', value: defaults.terminalTextShadowColor },
];
try {
@@ -5,12 +5,50 @@ import { NotificationService } from '../services/notification.service';
import { ipBlacklistService } from '../services/ip-blacklist.service';
import { exportConnectionsAsEncryptedZip } from '../services/import-export.service';
import { UpdateSidebarConfigDto, UpdateCaptchaSettingsDto, CaptchaSettings } from '../types/settings.types';
import { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types';
import { getAppearanceSettings, updateAppearanceSettings as updateAppearanceSettingsInRepo } from '../repositories/appearance.repository';
import i18next from '../i18n';
const auditLogService = new AuditLogService();
const notificationService = new NotificationService();
export const settingsController = {
/**
* 获取外观设置
*/
async getAppearanceSettings(req: Request, res: Response): Promise<void> {
try {
const settings = await getAppearanceSettings();
res.json(settings);
} catch (error: any) {
console.error('获取外观设置时出错:', error);
res.status(500).json({ message: '获取外观设置失败', error: error.message });
}
},
/**
* 更新外观设置
*/
async updateAppearanceSettings(req: Request, res: Response): Promise<void> {
try {
const settingsDto: UpdateAppearanceDto = req.body;
// 可在此处添加 DTO 验证逻辑
if (typeof settingsDto !== 'object' || settingsDto === null) {
res.status(400).json({ message: '无效的请求体,应为 JSON 对象' });
return;
}
const result = await updateAppearanceSettingsInRepo(settingsDto);
if (result) {
res.status(200).json({ message: '外观设置已成功更新' });
} else {
// 如果仓库层返回 false,可能表示没有实际更改或更新失败
res.status(200).json({ message: '外观设置未发生更改或更新失败' });
}
} catch (error: any) {
console.error('更新外观设置时出错:', error);
res.status(500).json({ message: '更新外观设置失败', error: error.message });
}
},
/**
* 获取所有设置项
*/
@@ -15,6 +15,11 @@ router.use(isAuthenticated);
router.get('/', settingsController.getAllSettings); // GET /api/v1/settings
router.put('/', settingsController.updateSettings); // PUT /api/v1/settings
// +++ 外观设置路由 +++
// GET /api/v1/settings/appearance - 获取外观设置
router.get('/appearance', settingsController.getAppearanceSettings);
// PUT /api/v1/settings/appearance - 更新外观设置
router.put('/appearance', settingsController.updateAppearanceSettings);
// +++ 焦点切换顺序路由 +++
// GET /api/v1/settings/focus-switcher-sequence - 获取焦点切换顺序
router.get('/focus-switcher-sequence', settingsController.getFocusSwitcherSequence);
@@ -21,6 +21,14 @@ export interface AppearanceSettings {
terminalBackgroundOverlayOpacity?: number; // 终端背景蒙版透明度 (0-1)
terminal_custom_html?: string; // 用户自定义终端背景 HTML
remoteHtmlPresetsUrl?: string | null; // 远程 HTML 预设仓库 URL
terminalTextStrokeEnabled?: boolean;
terminalTextStrokeWidth?: number;
terminalTextStrokeColor?: string;
terminalTextShadowEnabled?: boolean;
terminalTextShadowOffsetX?: number;
terminalTextShadowOffsetY?: number;
terminalTextShadowBlur?: number;
terminalTextShadowColor?: string;
updatedAt?: number;
}
@@ -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>
+17 -1
View File
@@ -147,7 +147,23 @@
"noMatchingRemotePresetsFound": "No matching remote themes found",
"editAsNewTooltip": "Edit as new custom theme",
"presetTag": "Preset",
"customTag": "Custom"
"customTag": "Custom",
"textStrokeSettingsSaved": "Text stroke settings saved.",
"textStrokeSettingsSaveFailed": "Failed to save text stroke settings: {message}",
"textShadowSettingsSaved": "Text shadow settings saved.",
"textShadowSettingsSaveFailed": "Failed to save text shadow settings: {message}",
"textStrokeSettings": "Text Stroke Settings",
"enableTextStroke": "Enable Text Stroke",
"textStrokeWidth": "Stroke Width (px)",
"textStrokeColor": "Stroke Color",
"saveStrokeSettings": "Save Stroke Settings",
"textShadowSettings": "Text Shadow Settings",
"enableTextShadow": "Enable Text Shadow",
"textShadowOffsetX": "Shadow X Offset (px)",
"textShadowOffsetY": "Shadow Y Offset (px)",
"textShadowBlur": "Shadow Blur Radius (px)",
"textShadowColor": "Shadow Color",
"saveShadowSettings": "Save Shadow Settings"
},
"login": {
"title": "User Login",
+18 -1
View File
@@ -1334,7 +1334,24 @@
"noMatchingRemotePresetsFound": "一致するリモートテーマが見つかりませんでした",
"editAsNewTooltip": "新しいカスタムテーマとして編集",
"presetTag": "プリセット",
"customTag": "カスタム"
"customTag": "カスタム",
"customHtmlResetSuccess": "カスタム HTML がリセットされました。",
"textStrokeSettingsSaved": "文字の縁取り設定が保存されました。",
"textStrokeSettingsSaveFailed": "文字の縁取り設定の保存に失敗しました: {message}",
"textShadowSettingsSaved": "文字の影設定が保存されました。",
"textShadowSettingsSaveFailed": "文字の影設定の保存に失敗しました: {message}",
"textStrokeSettings": "文字の縁取り設定",
"enableTextStroke": "文字の縁取りを有効にする",
"textStrokeWidth": "縁取りの太さ (px)",
"textStrokeColor": "縁取りの色",
"saveStrokeSettings": "縁取り設定を保存",
"textShadowSettings": "文字の影設定",
"enableTextShadow": "文字の影を有効にする",
"textShadowOffsetX": "影の X オフセット (px)",
"textShadowOffsetY": "影の Y オフセット (px)",
"textShadowBlur": "影のぼかし半径 (px)",
"textShadowColor": "影の色",
"saveShadowSettings": "影の設定を保存"
},
"tags": {
+17 -1
View File
@@ -148,7 +148,23 @@
"editAsNewTooltip": "编辑为新自定义主题",
"presetTag": "预设",
"customTag": "自定义",
"customHtmlResetSuccess":"自定义 HTML 已重置。"
"customHtmlResetSuccess":"自定义 HTML 已重置。",
"textStrokeSettingsSaved": "文字描边设置已保存。",
"textStrokeSettingsSaveFailed": "保存文字描边设置失败: {message}",
"textShadowSettingsSaved": "文字阴影设置已保存。",
"textShadowSettingsSaveFailed": "保存文字阴影设置失败: {message}",
"textStrokeSettings": "文字描边设置",
"enableTextStroke": "启用文字描边",
"textStrokeWidth": "描边粗细 (px)",
"textStrokeColor": "描边颜色",
"saveStrokeSettings": "保存描边设置",
"textShadowSettings": "文字阴影设置",
"enableTextShadow": "启用文字阴影",
"textShadowOffsetX": "阴影 X 偏移 (px)",
"textShadowOffsetY": "阴影 Y 偏移 (px)",
"textShadowBlur": "阴影模糊半径 (px)",
"textShadowColor": "阴影颜色",
"saveShadowSettings": "保存阴影设置"
},
"login": {
"title": "用户登录",
@@ -145,6 +145,34 @@ export const useAppearanceStore = defineStore('appearance', () => {
// 获取终端自定义 CSS
const terminalCustomHTML = computed(() => appearanceSettings.value.terminal_custom_html ?? null);
// 文字描边设置
const terminalTextStrokeEnabled = computed<boolean>(() => {
return appearanceSettings.value.terminalTextStrokeEnabled ?? false;
});
const terminalTextStrokeWidth = computed<number>(() => {
return appearanceSettings.value.terminalTextStrokeWidth ?? 1;
});
const terminalTextStrokeColor = computed<string>(() => {
return appearanceSettings.value.terminalTextStrokeColor ?? '#000000';
});
// 文字阴影设置
const terminalTextShadowEnabled = computed<boolean>(() => {
return appearanceSettings.value.terminalTextShadowEnabled ?? false;
});
const terminalTextShadowOffsetX = computed<number>(() => {
return appearanceSettings.value.terminalTextShadowOffsetX ?? 0;
});
const terminalTextShadowOffsetY = computed<number>(() => {
return appearanceSettings.value.terminalTextShadowOffsetY ?? 0;
});
const terminalTextShadowBlur = computed<number>(() => {
return appearanceSettings.value.terminalTextShadowBlur ?? 0;
});
const terminalTextShadowColor = computed<string>(() => {
return appearanceSettings.value.terminalTextShadowColor ?? 'rgba(0,0,0,0.5)';
});
// --- Actions ---
/**
@@ -202,7 +230,15 @@ export const useAppearanceStore = defineStore('appearance', () => {
async function updateAppearanceSettings(updates: UpdateAppearanceDto) {
try {
// 移除预设主题闪烁修复逻辑,不再需要
const response = await apiClient.put<AppearanceSettings>('/appearance', updates); // 使用 apiClient
// Construct the full payload to send to the backend
// This includes all current settings merged with the specific updates
const payloadToSend: Partial<AppearanceSettings> = {
...appearanceSettings.value, // Start with all current settings from the store
...updates // Apply the specific changes passed to this function
};
const response = await apiClient.put<AppearanceSettings>('/appearance', payloadToSend); // 使用 apiClient, 发送合并后的 payload
// 使用后端返回的最新设置更新本地状态
appearanceSettings.value = response.data;
console.log('[AppearanceStore] 外观设置已更新:', appearanceSettings.value);
@@ -338,6 +374,34 @@ export const useAppearanceStore = defineStore('appearance', () => {
}
}
// --- 文字描边 Actions ---
async function setTerminalTextStrokeEnabled(enabled: boolean) {
await updateAppearanceSettings({ terminalTextStrokeEnabled: enabled });
}
async function setTerminalTextStrokeWidth(width: number) {
await updateAppearanceSettings({ terminalTextStrokeWidth: width });
}
async function setTerminalTextStrokeColor(color: string) {
await updateAppearanceSettings({ terminalTextStrokeColor: color });
}
// --- 文字阴影 Actions ---
async function setTerminalTextShadowEnabled(enabled: boolean) {
await updateAppearanceSettings({ terminalTextShadowEnabled: enabled });
}
async function setTerminalTextShadowOffsetX(offset: number) {
await updateAppearanceSettings({ terminalTextShadowOffsetX: offset });
}
async function setTerminalTextShadowOffsetY(offset: number) {
await updateAppearanceSettings({ terminalTextShadowOffsetY: offset });
}
async function setTerminalTextShadowBlur(blur: number) {
await updateAppearanceSettings({ terminalTextShadowBlur: blur });
}
async function setTerminalTextShadowColor(color: string) {
await updateAppearanceSettings({ terminalTextShadowColor: color });
}
// --- 终端主题列表管理 Actions ---
/**
@@ -847,13 +911,33 @@ export const useAppearanceStore = defineStore('appearance', () => {
uploadTerminalBackground,
setTerminalBackgroundOverlayOpacity,
setTerminalCustomHTML, // 设置终端自定义 HTML
// 文字描边 Actions
setTerminalTextStrokeEnabled,
setTerminalTextStrokeWidth,
setTerminalTextStrokeColor,
// 文字阴影 Actions
setTerminalTextShadowEnabled,
setTerminalTextShadowOffsetX,
setTerminalTextShadowOffsetY,
setTerminalTextShadowBlur,
setTerminalTextShadowColor,
removePageBackground,
removeTerminalBackground,
loadTerminalThemeData,
loadTerminalThemeData,
isTerminalBackgroundEnabled,
terminalCustomHTML, // 获取终端自定义 HTML
// 文字描边 Computed
terminalTextStrokeEnabled,
terminalTextStrokeWidth,
terminalTextStrokeColor,
// 文字阴影 Computed
terminalTextShadowEnabled,
terminalTextShadowOffsetX,
terminalTextShadowOffsetY,
terminalTextShadowBlur,
terminalTextShadowColor,
startTerminalThemePreview,
stopTerminalThemePreview,
stopTerminalThemePreview,
// Visibility control
isStyleCustomizerVisible,
toggleStyleCustomizer,
@@ -17,6 +17,18 @@ export interface AppearanceSettings {
terminalBackgroundOverlayOpacity?: number; // 终端背景蒙版透明度 (0-1)
terminal_custom_html?: string | null; // 终端自定义 HTML
remoteHtmlPresetsUrl?: string | null; // 远程 HTML 主题仓库链接
// 文字描边
terminalTextStrokeEnabled?: boolean;
terminalTextStrokeWidth?: number;
terminalTextStrokeColor?: string;
// 文字阴影
terminalTextShadowEnabled?: boolean;
terminalTextShadowOffsetX?: number;
terminalTextShadowOffsetY?: number;
terminalTextShadowBlur?: number;
terminalTextShadowColor?: string;
}
// 前端用于更新外观设置的数据结构 (对应 API 请求体)