feat: 添加自定义终端背景html功能
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, nextTick, reactive } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { TerminalTheme } from '../types/terminal-theme.types';
|
||||
import StyleCustomizerUiTab from './style-customizer/StyleCustomizerUiTab.vue';
|
||||
@@ -42,15 +42,102 @@ const handleResetUiTheme = async () => {
|
||||
|
||||
|
||||
const modalRootRef = ref<HTMLDivElement | null>(null);
|
||||
const headerRef = ref<HTMLElement | null>(null);
|
||||
const dialogContentRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const draggableState = reactive({
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
initialLeft: 0,
|
||||
initialTop: 0,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const headerElement = headerRef.value;
|
||||
const dialogEl = dialogContentRef.value;
|
||||
const rootEl = modalRootRef.value;
|
||||
|
||||
if (!headerElement || !dialogEl || !rootEl) {
|
||||
// console.warn("Draggable elements not found for StyleCustomizer modal."); // 用于调试的可选日志
|
||||
return;
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
//确保对话框已渲染且其尺寸可用
|
||||
if (dialogEl && rootEl) {
|
||||
dialogEl.style.position = 'absolute'; // 使对话框在rootEl内绝对定位
|
||||
const rootWidth = rootEl.clientWidth;
|
||||
const rootHeight = rootEl.clientHeight;
|
||||
const dialogWidth = dialogEl.offsetWidth;
|
||||
const dialogHeight = dialogEl.offsetHeight;
|
||||
|
||||
// 使对话框居中
|
||||
dialogEl.style.left = `${Math.max(0, (rootWidth - dialogWidth) / 2)}px`;
|
||||
dialogEl.style.top = `${Math.max(0, (rootHeight - dialogHeight) / 2)}px`;
|
||||
}
|
||||
});
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
if (!dialogEl) return;
|
||||
event.preventDefault(); // 防止文本选择等默认行为
|
||||
|
||||
draggableState.isDragging = true;
|
||||
draggableState.startX = event.clientX;
|
||||
draggableState.startY = event.clientY;
|
||||
draggableState.initialLeft = dialogEl.offsetLeft;
|
||||
draggableState.initialTop = dialogEl.offsetTop;
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => {
|
||||
if (!draggableState.isDragging || !dialogEl || !rootEl) return;
|
||||
|
||||
const dx = event.clientX - draggableState.startX;
|
||||
const dy = event.clientY - draggableState.startY;
|
||||
|
||||
let newLeft = draggableState.initialLeft + dx;
|
||||
let newTop = draggableState.initialTop + dy;
|
||||
|
||||
// 边界检查,使对话框保持在 modalRootRef (视口) 内
|
||||
newLeft = Math.max(0, Math.min(newLeft, rootEl.clientWidth - dialogEl.offsetWidth));
|
||||
newTop = Math.max(0, Math.min(newTop, rootEl.clientHeight - dialogEl.offsetHeight));
|
||||
|
||||
dialogEl.style.left = `${newLeft}px`;
|
||||
dialogEl.style.top = `${newTop}px`;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
if (!draggableState.isDragging) return;
|
||||
draggableState.isDragging = false;
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
headerElement.addEventListener('mousedown', onMouseDown);
|
||||
headerElement.style.cursor = 'move'; // 设置页眉鼠标样式为可拖动
|
||||
|
||||
onUnmounted(() => {
|
||||
if (headerElement) {
|
||||
headerElement.removeEventListener('mousedown', onMouseDown);
|
||||
headerElement.style.cursor = ''; // 重置鼠标样式
|
||||
}
|
||||
// 清理全局监听器
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div ref="modalRootRef" class="fixed inset-0 bg-black/60 flex justify-center items-center z-[1000] p-2 md:p-4" @click.self="closeCustomizer">
|
||||
<div class="bg-background text-foreground rounded-lg shadow-lg w-full h-full md:w-[90%] md:max-w-[800px] md:h-[85vh] md:max-h-[700px] flex flex-col overflow-hidden">
|
||||
<header class="flex justify-between items-center px-4 py-3 border-b border-border bg-header flex-shrink-0">
|
||||
<div ref="modalRootRef" class="fixed inset-0 z-[1000]" @click.self="closeCustomizer">
|
||||
<div ref="dialogContentRef" class="bg-background text-foreground rounded-lg shadow-lg w-full h-full md:w-[90%] md:max-w-[800px] md:h-[85vh] md:max-h-[700px] flex flex-col overflow-hidden">
|
||||
<header ref="headerRef" class="flex justify-between items-center px-4 py-3 border-b border-border bg-header flex-shrink-0">
|
||||
<h2 class="m-0 text-lg md:text-xl text-foreground">{{ t('styleCustomizer.title') }}</h2>
|
||||
<button @click="closeCustomizer" class="bg-transparent border-none text-2xl md:text-3xl leading-none cursor-pointer text-text-secondary px-2 py-1 rounded hover:text-foreground hover:bg-black/10">×</button>
|
||||
</header>
|
||||
|
||||
@@ -29,6 +29,7 @@ const terminalOuterWrapperRef = ref<HTMLElement | null>(null); // 最外层容
|
||||
let terminal: Terminal | null = null;
|
||||
let fitAddon: FitAddon | null = null;
|
||||
let searchAddon: SearchAddon | null = null; // *** 添加 searchAddon 变量 ***
|
||||
const customHtmlLayerRef = ref<HTMLElement | null>(null); // Ref for the custom HTML layer
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let observedElement: HTMLElement | null = null; // +++ Store the observed element +++
|
||||
let debounceTimer: number | null = null; // 用于防抖的计时器 ID
|
||||
@@ -49,6 +50,7 @@ const {
|
||||
currentTerminalFontSize,
|
||||
isTerminalBackgroundEnabled,
|
||||
currentTerminalBackgroundOverlayOpacity, // 获取蒙版透明度
|
||||
terminalCustomHTML, // 用于自定义终端背景 HTML
|
||||
} = storeToRefs(appearanceStore);
|
||||
|
||||
// --- Settings Store ---
|
||||
@@ -614,7 +616,28 @@ defineExpose({ write, findNext, findPrevious, clearSearch, clear }); // 暴露 c
|
||||
const applyTerminalBackground = () => {
|
||||
// 背景应用到 terminalOuterWrapperRef
|
||||
if (terminalOuterWrapperRef.value) {
|
||||
if (!isTerminalBackgroundEnabled.value) {
|
||||
if (isTerminalBackgroundEnabled.value) {
|
||||
// 只要启用了背景功能,就应该让 xterm 透明以显示下方内容
|
||||
nextTick(() => {
|
||||
if (terminalOuterWrapperRef.value) {
|
||||
terminalOuterWrapperRef.value.classList.add('has-terminal-background');
|
||||
if (terminalBackgroundImage.value) {
|
||||
const backendUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
const imagePath = terminalBackgroundImage.value;
|
||||
const fullImageUrl = `${backendUrl}${imagePath}`;
|
||||
terminalOuterWrapperRef.value.style.backgroundImage = `url(${fullImageUrl})`;
|
||||
terminalOuterWrapperRef.value.style.backgroundSize = 'cover';
|
||||
terminalOuterWrapperRef.value.style.backgroundPosition = 'center';
|
||||
terminalOuterWrapperRef.value.style.backgroundRepeat = 'no-repeat';
|
||||
console.log(`[Terminal ${props.sessionId}] 应用终端背景图片: ${terminalBackgroundImage.value}`);
|
||||
} else {
|
||||
terminalOuterWrapperRef.value.style.backgroundImage = 'none';
|
||||
console.log(`[Terminal ${props.sessionId}] 终端背景功能已启用,但无背景图片,xterm 应透明。`);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 背景功能禁用
|
||||
nextTick(() => {
|
||||
if (terminalOuterWrapperRef.value) {
|
||||
terminalOuterWrapperRef.value.style.backgroundImage = 'none';
|
||||
@@ -622,45 +645,82 @@ const applyTerminalBackground = () => {
|
||||
}
|
||||
});
|
||||
console.log(`[Terminal ${props.sessionId}] 终端背景已禁用,移除背景。`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (terminalBackgroundImage.value) {
|
||||
const backendUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
const imagePath = terminalBackgroundImage.value;
|
||||
const fullImageUrl = `${backendUrl}${imagePath}`;
|
||||
nextTick(() => {
|
||||
if (terminalOuterWrapperRef.value) {
|
||||
terminalOuterWrapperRef.value.style.backgroundImage = `url(${fullImageUrl})`;
|
||||
terminalOuterWrapperRef.value.style.backgroundSize = 'cover';
|
||||
terminalOuterWrapperRef.value.style.backgroundPosition = 'center';
|
||||
terminalOuterWrapperRef.value.style.backgroundRepeat = 'no-repeat';
|
||||
terminalOuterWrapperRef.value.classList.add('has-terminal-background');
|
||||
}
|
||||
});
|
||||
console.log(`[Terminal ${props.sessionId}] 应用终端背景图片: ${terminalBackgroundImage.value}`);
|
||||
} else {
|
||||
nextTick(() => {
|
||||
if (terminalOuterWrapperRef.value) {
|
||||
terminalOuterWrapperRef.value.style.backgroundImage = 'none';
|
||||
terminalOuterWrapperRef.value.classList.remove('has-terminal-background');
|
||||
}
|
||||
});
|
||||
console.log(`[Terminal ${props.sessionId}] 移除终端背景图片。`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Function to execute scripts within an element
|
||||
const executeScriptsInElement = (container: HTMLElement) => {
|
||||
if (!container) return;
|
||||
console.log('[Terminal] Attempting to execute scripts in custom HTML container:', container);
|
||||
|
||||
const scripts = Array.from(container.getElementsByTagName('script'));
|
||||
console.log(`[Terminal] Found ${scripts.length} script(s) in custom HTML.`);
|
||||
|
||||
scripts.forEach((oldScript, index) => {
|
||||
console.log(`[Terminal] Processing script #${index + 1}:`, oldScript.outerHTML.substring(0, 100) + '...');
|
||||
const newScript = document.createElement('script');
|
||||
|
||||
// Copy attributes (type, src, async, defer, etc.)
|
||||
Array.from(oldScript.attributes).forEach(attr => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
console.log(`[Terminal] Script #${index + 1}: Copied attribute ${attr.name}="${attr.value}"`);
|
||||
});
|
||||
|
||||
// Copy content for inline scripts
|
||||
if (oldScript.textContent) {
|
||||
newScript.textContent = oldScript.textContent;
|
||||
console.log(`[Terminal] Script #${index + 1}: Copied inline content.`);
|
||||
}
|
||||
|
||||
if (oldScript.parentNode) {
|
||||
oldScript.parentNode.insertBefore(newScript, oldScript.nextSibling); // Insert new after old
|
||||
oldScript.parentNode.removeChild(oldScript); // Then remove old
|
||||
console.log('[Terminal] Script #${index + 1} re-inserted and old one removed.');
|
||||
} else {
|
||||
container.appendChild(newScript);
|
||||
console.warn('[Terminal] Script #${index + 1} had no parent, appended to container directly.');
|
||||
}
|
||||
});
|
||||
console.log('[Terminal] Finished processing scripts in custom HTML.');
|
||||
};
|
||||
|
||||
// Watch for changes in terminalCustomHTML and execute scripts
|
||||
watch(terminalCustomHTML, (newHtmlContent) => {
|
||||
// Always operate within nextTick to ensure v-html has updated the DOM
|
||||
nextTick(() => {
|
||||
const container = customHtmlLayerRef.value;
|
||||
if (container) {
|
||||
if (newHtmlContent) {
|
||||
console.log('[Terminal] terminalCustomHTML changed, processing new HTML content.');
|
||||
executeScriptsInElement(container);
|
||||
} else {
|
||||
console.log('[Terminal] terminalCustomHTML cleared.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="terminalOuterWrapperRef" class="terminal-outer-wrapper">
|
||||
<!-- 蒙版层 -->
|
||||
<div
|
||||
v-if="isTerminalBackgroundEnabled && terminalBackgroundImage"
|
||||
v-if="isTerminalBackgroundEnabled"
|
||||
class="terminal-background-overlay"
|
||||
:style="{ backgroundColor: `rgba(0, 0, 0, ${currentTerminalBackgroundOverlayOpacity})` }"
|
||||
></div>
|
||||
<div
|
||||
ref="customHtmlLayerRef"
|
||||
v-if="isTerminalBackgroundEnabled && terminalCustomHTML"
|
||||
class="terminal-custom-html-layer"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none;"
|
||||
v-html="terminalCustomHTML"
|
||||
></div>
|
||||
<!-- xterm 实际挂载点 -->
|
||||
<div ref="terminalRef" class="terminal-inner-container"></div>
|
||||
</div>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -134,6 +134,9 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
||||
return typeof opacity === 'number' && opacity >= 0 && opacity <= 1 ? opacity : 0.5; // 默认 0.5
|
||||
});
|
||||
|
||||
// 获取终端自定义 CSS
|
||||
const terminalCustomHTML = computed(() => appearanceSettings.value.terminal_custom_html ?? null);
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
/**
|
||||
@@ -307,6 +310,22 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
||||
await updateAppearanceSettings({ terminalBackgroundOverlayOpacity: opacity });
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置终端自定义 HTML
|
||||
* @param html HTML 字符串,或 null 清除
|
||||
*/
|
||||
async function setTerminalCustomHTML(html: string | null) {
|
||||
try {
|
||||
await updateAppearanceSettings({ terminal_custom_html: html });
|
||||
// console.log('[AppearanceStore] Terminal custom HTML updated successfully.');
|
||||
// 可以在此调用 uiNotifications.store 来显示成功消息
|
||||
} catch (err: any) {
|
||||
console.error('设置终端自定义 HTML 失败:', err);
|
||||
// 可以在此调用 uiNotifications.store 来显示失败消息
|
||||
throw new Error(err.response?.data?.message || err.message || '设置终端自定义 HTML 失败');
|
||||
}
|
||||
}
|
||||
|
||||
// --- 终端主题列表管理 Actions ---
|
||||
|
||||
/**
|
||||
@@ -675,11 +694,13 @@ export const useAppearanceStore = defineStore('appearance', () => {
|
||||
exportTerminalTheme,
|
||||
uploadPageBackground,
|
||||
uploadTerminalBackground,
|
||||
setTerminalBackgroundOverlayOpacity,
|
||||
setTerminalBackgroundOverlayOpacity,
|
||||
setTerminalCustomHTML, // 设置终端自定义 HTML
|
||||
removePageBackground,
|
||||
removeTerminalBackground,
|
||||
loadTerminalThemeData,
|
||||
isTerminalBackgroundEnabled,
|
||||
terminalCustomHTML, // 获取终端自定义 HTML
|
||||
startTerminalThemePreview,
|
||||
stopTerminalThemePreview,
|
||||
// Visibility control
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface AppearanceSettings {
|
||||
editorFontFamily?: string | null; // Monaco Editor 字体偏好
|
||||
terminalBackgroundEnabled?: boolean; // 终端背景是否启用
|
||||
terminalBackgroundOverlayOpacity?: number; // 终端背景蒙版透明度 (0-1)
|
||||
terminal_custom_html?: string | null; // 终端自定义 HTML
|
||||
}
|
||||
|
||||
// 前端用于更新外观设置的数据结构 (对应 API 请求体)
|
||||
|
||||
Reference in New Issue
Block a user