feat: 添加自定义终端字体描边和阴影设置项
This commit is contained in:
@@ -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 });
|
||||
|
||||
|
||||
@@ -51,6 +51,15 @@ const {
|
||||
isTerminalBackgroundEnabled,
|
||||
currentTerminalBackgroundOverlayOpacity, // 获取蒙版透明度
|
||||
terminalCustomHTML, // 用于自定义终端背景 HTML
|
||||
// --- 文字描边和阴影状态 ---
|
||||
terminalTextStrokeEnabled,
|
||||
terminalTextStrokeWidth,
|
||||
terminalTextStrokeColor,
|
||||
terminalTextShadowEnabled,
|
||||
terminalTextShadowOffsetX,
|
||||
terminalTextShadowOffsetY,
|
||||
terminalTextShadowBlur,
|
||||
terminalTextShadowColor,
|
||||
} = storeToRefs(appearanceStore);
|
||||
|
||||
// --- Settings Store ---
|
||||
@@ -612,6 +621,55 @@ 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,6 +493,68 @@ 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">
|
||||
<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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,11 +911,31 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
||||
uploadTerminalBackground,
|
||||
setTerminalBackgroundOverlayOpacity,
|
||||
setTerminalCustomHTML, // 设置终端自定义 HTML
|
||||
// 文字描边 Actions
|
||||
setTerminalTextStrokeEnabled,
|
||||
setTerminalTextStrokeWidth,
|
||||
setTerminalTextStrokeColor,
|
||||
// 文字阴影 Actions
|
||||
setTerminalTextShadowEnabled,
|
||||
setTerminalTextShadowOffsetX,
|
||||
setTerminalTextShadowOffsetY,
|
||||
setTerminalTextShadowBlur,
|
||||
setTerminalTextShadowColor,
|
||||
removePageBackground,
|
||||
removeTerminalBackground,
|
||||
loadTerminalThemeData,
|
||||
isTerminalBackgroundEnabled,
|
||||
terminalCustomHTML, // 获取终端自定义 HTML
|
||||
// 文字描边 Computed
|
||||
terminalTextStrokeEnabled,
|
||||
terminalTextStrokeWidth,
|
||||
terminalTextStrokeColor,
|
||||
// 文字阴影 Computed
|
||||
terminalTextShadowEnabled,
|
||||
terminalTextShadowOffsetX,
|
||||
terminalTextShadowOffsetY,
|
||||
terminalTextShadowBlur,
|
||||
terminalTextShadowColor,
|
||||
startTerminalThemePreview,
|
||||
stopTerminalThemePreview,
|
||||
// Visibility control
|
||||
|
||||
@@ -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 请求体)
|
||||
|
||||
Reference in New Issue
Block a user