feat: 添加自定义终端背景html功能

This commit is contained in:
Baobhan Sith
2025-05-27 09:33:25 +08:00
parent 180a5f6182
commit e11cc66114
9 changed files with 268 additions and 119 deletions
@@ -12,10 +12,12 @@ const {
terminalBackgroundImage,
isTerminalBackgroundEnabled,
currentTerminalBackgroundOverlayOpacity,
terminalCustomHTML,
} = storeToRefs(appearanceStore);
const localTerminalBackgroundEnabled = ref(true);
const editableTerminalBackgroundOverlayOpacity = ref(0.5);
const localTerminalCustomHTML = ref('');
const terminalBgFileInput = ref<HTMLInputElement | null>(null);
const uploadError = ref<string | null>(null);
@@ -23,6 +25,7 @@ const uploadError = ref<string | null>(null);
const initializeEditableState = () => {
localTerminalBackgroundEnabled.value = isTerminalBackgroundEnabled.value;
editableTerminalBackgroundOverlayOpacity.value = currentTerminalBackgroundOverlayOpacity.value;
localTerminalCustomHTML.value = terminalCustomHTML.value || '';
uploadError.value = null;
};
@@ -40,6 +43,13 @@ watch(currentTerminalBackgroundOverlayOpacity, (newValue) => {
}
});
watch(terminalCustomHTML, (newValue) => {
if (localTerminalCustomHTML.value !== (newValue || '')) {
localTerminalCustomHTML.value = newValue || '';
}
});
const handleTriggerTerminalBgUpload = () => {
uploadError.value = null;
terminalBgFileInput.value?.click();
@@ -104,6 +114,17 @@ const handleSaveTerminalBackgroundOverlayOpacity = async () => {
}
};
const handleSaveCustomHTML = async () => {
try {
await appearanceStore.setTerminalCustomHTML(localTerminalCustomHTML.value);
notificationsStore.addNotification({ type: 'success', message: t('styleCustomizer.customTerminalHTMLSaved') });
} catch (error: any) {
console.error("保存自定义终端 HTML 失败:", error);
notificationsStore.addNotification({ type: 'error', message: t('styleCustomizer.customTerminalHTMLSaveFailed', { message: error.message }) });
}
};
</script>
<template>
@@ -171,6 +192,26 @@ const handleSaveTerminalBackgroundOverlayOpacity = async () => {
<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>
</div>
<!-- 自定义终端背景 HTML -->
<div class="mt-4 pt-4 border-t border-border/50">
<label for="terminalCustomHTML" class="block text-sm font-medium text-foreground mb-1">{{ t('styleCustomizer.customTerminalHTML', '自定义终端背景 HTML') }}</label>
<textarea
id="terminalCustomHTML"
v-model="localTerminalCustomHTML"
rows="10"
class="w-full p-2 border border-border rounded bg-input text-foreground focus:ring-primary focus:border-primary"
:placeholder="t('styleCustomizer.customTerminalHTMLPlaceholder', '例如:<h1>Hello</h1>')"
></textarea>
<div class="mt-2 flex justify-end">
<button
@click="handleSaveCustomHTML"
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>
</div>
<div v-else class="p-4 text-center text-text-secondary italic border border-dashed border-border/50 rounded-md">
{{ t('styleCustomizer.terminalBgDisabled', '终端背景功能已禁用。') }}
@@ -57,10 +57,6 @@ brightMagenta: #ff55ff
brightCyan: #55ffff
brightWhite: #ffffff`;
// Theme preview refs
const originalModalRootBackgroundColor = ref<string | null>(null);
const originalModalContentOpacity = ref<string | null>(null);
const originalModalRootTransition = ref<string | null>(null);
const originalModalContentTransition = ref<string | null>(null);
const initializeEditableState = () => {
editableTerminalFontFamily.value = currentTerminalFontFamily.value;
@@ -341,73 +337,6 @@ const handleFocusAndSelect = (event: FocusEvent) => {
}
};
const handlePreviewButtonMouseDown = async (event: MouseEvent, themeToPreview: TerminalTheme) => {
event.preventDefault();
if (props.modalRootRef && themeToPreview._id) {
const modalContentElement = props.modalRootRef.firstElementChild as HTMLElement;
try {
const themeData = await appearanceStore.loadTerminalThemeData(themeToPreview._id);
if (!themeData) {
console.error('Preview failed: Could not load theme data for', themeToPreview.name);
return;
}
appearanceStore.startTerminalThemePreview(themeData);
if (originalModalRootBackgroundColor.value === null && modalContentElement) {
originalModalRootBackgroundColor.value = window.getComputedStyle(props.modalRootRef).backgroundColor;
originalModalRootTransition.value = props.modalRootRef.style.transition;
originalModalContentOpacity.value = window.getComputedStyle(modalContentElement).opacity;
originalModalContentTransition.value = modalContentElement.style.transition;
}
props.modalRootRef.style.transition = 'background-color 0.05s ease-out, opacity 0.05s ease-out';
props.modalRootRef.style.backgroundColor = 'transparent';
if (modalContentElement) {
modalContentElement.style.transition = 'opacity 0.05s ease-out';
modalContentElement.style.opacity = '0';
}
const restoreOnMouseUp = () => {
appearanceStore.stopTerminalThemePreview();
if (props.modalRootRef) {
props.modalRootRef.style.backgroundColor = originalModalRootBackgroundColor.value || '';
props.modalRootRef.style.transition = originalModalRootTransition.value || '';
}
if (modalContentElement) {
modalContentElement.style.opacity = originalModalContentOpacity.value || '1';
modalContentElement.style.transition = originalModalContentTransition.value || '';
}
originalModalRootBackgroundColor.value = null;
originalModalContentOpacity.value = null;
originalModalRootTransition.value = null;
originalModalContentTransition.value = null;
document.removeEventListener('mouseup', restoreOnMouseUp);
const currentTargetButton = event.currentTarget as HTMLElement | null;
if (currentTargetButton) {
currentTargetButton.removeEventListener('mouseleave', restoreOnMouseUp);
}
};
document.addEventListener('mouseup', restoreOnMouseUp, { once: true });
const currentTargetButton = event.currentTarget as HTMLElement | null;
if (currentTargetButton) {
currentTargetButton.addEventListener('mouseleave', restoreOnMouseUp, { once: true });
}
} catch (error) {
console.error('Error during theme preview:', error);
appearanceStore.stopTerminalThemePreview();
if (props.modalRootRef && modalContentElement) {
props.modalRootRef.style.backgroundColor = originalModalRootBackgroundColor.value || '';
props.modalRootRef.style.transition = originalModalRootTransition.value || '';
modalContentElement.style.opacity = originalModalContentOpacity.value || '1';
modalContentElement.style.transition = originalModalContentTransition.value || '';
originalModalRootBackgroundColor.value = null;
originalModalContentOpacity.value = null;
originalModalRootTransition.value = null;
originalModalContentTransition.value = null;
}
}
}
};
// Computed Properties
const activeThemeName = computed(() => {
@@ -543,15 +472,6 @@ watch(() => props.isEditingTheme, (isEditing) => {
>
{{ t('styleCustomizer.applyButton', 'Apply') }}
</button>
<button
@mousedown="(event) => handlePreviewButtonMouseDown(event, theme)"
:class="[
'px-3 py-1.5 text-xs md:text-sm border rounded transition-colors duration-200 ease-in-out whitespace-nowrap',
theme._id === activeTerminalThemeId?.toString() ? 'text-button-text border-white/30 bg-white/10 hover:bg-white/20 hover:border-white/50' : 'border-border bg-header text-foreground hover:bg-border hover:border-text-secondary'
]"
>
{{ t('styleCustomizer.previewButton', 'Preview') }}
</button>
<button @click="handleEditTheme(theme)" :title="theme.isPreset ? t('styleCustomizer.editAsCopy', 'Edit as Copy') : t('common.edit')"
:class="[
'px-3 py-1.5 text-xs md:text-sm border rounded transition-colors duration-200 ease-in-out whitespace-nowrap disabled:opacity-60 disabled:cursor-not-allowed',