import { BaseDialogManager } from '../core/base-dialog-manager'; import { ManagerRegistry } from '../core/manager-registry'; import { t } from '../services/locale'; /** 渲染模式类型 */ type RenderMode = 'simple' | 'balance' | 'advanced'; /** 地面类型 */ type GroundType = 'none' | 'concrete' | 'grass' | 'tile' | 'water' | 'wood'; /** 环境背景类型 */ type EnvironmentType = 'default' | 'outdoor' | 'indoor' | 'night' | 'overcast' | 'studio'; // ======================== 通用样式常量 ======================== // 全部使用项目主题 CSS 变量(--bim-*),同时提供浅色/深色皆可见的 fallback /** 分节标签样式 */ const SECTION_LABEL_STYLE = 'font-size: 13px; color: var(--bim-text-secondary, #475569); margin-bottom: 8px;'; /** 分隔线样式 —— 使用 --bim-divider,浅色=#e2e8f0,深色=#334155 */ const DIVIDER_STYLE = 'height: 1px; background: var(--bim-divider, #e2e8f0); margin: 14px 0;'; /** 设置行样式(标签 + 控件水平排列) */ const ROW_STYLE = 'display: flex; align-items: center; justify-content: space-between; gap: 12px;'; /** 滑块输入框右侧数值样式 */ const SLIDER_VALUE_STYLE = 'font-size: 12px; color: var(--bim-text-secondary, #475569); min-width: 32px; text-align: right;'; /** 边框颜色 —— 使用 --bim-border-default */ const BORDER_COLOR = 'var(--bim-border-default, #e2e8f0)'; /** 输入框/下拉背景 —— 使用 --bim-bg-inset */ const INPUT_BG = 'var(--bim-bg-inset, #f1f5f9)'; /** 主色 */ const PRIMARY = 'var(--bim-primary, #3b82f6)'; /** 文字主色 */ const TEXT_PRIMARY = 'var(--bim-text-primary, #0f172a)'; /** 反色文字 */ const TEXT_INVERSE = 'var(--bim-text-inverse, #ffffff)'; /** 悬停背景 */ const HOVER_BG = 'var(--bim-component-bg-hover, rgba(0,0,0,0.04))'; /** * 设置弹窗管理器 * 管理全局设置面板,包含渲染模式、边线、地面、对比度/饱和度、光照强度、环境背景等设置项 */ export class SettingDialogManager extends BaseDialogManager { protected get dialogId() { return 'setting-dialog'; } protected get dialogTitle() { return 'setting.dialogTitle'; } protected get dialogWidth() { return 320; } // ================ 本地 UI 状态(不持久化) ================ /** 边线开关状态 */ private edgeLineEnabled: boolean = false; /** 当前选中的地面类型 */ private groundType: GroundType = 'none'; /** 地面大小 */ private groundSize: number = 100; /** 对比度(0-200,100为默认值) */ private contrastValue: number = 100; /** 饱和度(0-200,100为默认值) */ private saturationValue: number = 100; /** 光照强度(0-200,100为默认值) */ private lightIntensityValue: number = 100; /** 当前选中的环境背景 */ private environmentType: EnvironmentType = 'default'; constructor(registry: ManagerRegistry) { super(registry); } public init(): void { } /** 对话框居中显示 */ protected getDialogPosition() { const container = this.registry.container; if (!container) return { x: 100, y: 100 }; const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; return { x: (containerWidth - this.dialogWidth) / 2, y: Math.max(20, (containerHeight - 520) / 2) }; } // ======================== 主内容构建 ======================== protected createContent(): HTMLElement { // 注入一次全局滑块样式 this.ensureSliderStyle(); const content = document.createElement('div'); content.style.cssText = 'padding: 16px; max-height: 480px; overflow-y: auto;'; // 1. 渲染模式(按钮组) content.appendChild(this.createRenderModeSection()); content.appendChild(this.createDivider()); // 2. 边线开关 content.appendChild(this.createEdgeLineSection()); content.appendChild(this.createDivider()); // 3. 光照强度 content.appendChild(this.createSliderSection( t('setting.lightIntensity'), this.lightIntensityValue, 0, 200, (v) => { this.lightIntensityValue = v; } )); // 4. 对比度 content.appendChild(this.createSliderSection( t('setting.contrast'), this.contrastValue, 0, 200, (v) => { this.contrastValue = v; } )); // 5. 饱和度 content.appendChild(this.createSliderSection( t('setting.saturation'), this.saturationValue, 0, 200, (v) => { this.saturationValue = v; } )); content.appendChild(this.createDivider()); // 6. 环境背景(下拉) content.appendChild(this.createEnvironmentSection()); content.appendChild(this.createDivider()); // 7. 显示地面(下拉 + 大小输入) content.appendChild(this.createGroundSection()); return content; } // ======================== 分隔线 ======================== /** 创建分隔线 */ private createDivider(): HTMLElement { const div = document.createElement('div'); div.style.cssText = DIVIDER_STYLE; return div; } // ======================== 1. 渲染模式(按钮组) ======================== /** 创建渲染模式按钮组 */ private createRenderModeSection(): HTMLElement { const currentMode = (this.registry.engine3d?.getRenderMode() ?? 'balance') as RenderMode; const section = document.createElement('div'); // 标签 const label = document.createElement('div'); label.style.cssText = SECTION_LABEL_STYLE; label.textContent = t('setting.renderMode'); section.appendChild(label); // 按钮组容器 const group = document.createElement('div'); group.style.cssText = ` display: flex; gap: 0; border-radius: 6px; overflow: hidden; border: 1px solid ${BORDER_COLOR}; `; const modes: { key: RenderMode; labelKey: string }[] = [ { key: 'simple', labelKey: 'setting.modes.simple' }, { key: 'balance', labelKey: 'setting.modes.balance' }, { key: 'advanced', labelKey: 'setting.modes.advanced' }, ]; for (let i = 0; i < modes.length; i++) { const mode = modes[i]; const isActive = mode.key === currentMode; const btn = document.createElement('div'); btn.style.cssText = ` flex: 1; text-align: center; padding: 7px 0; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.15s; background: ${isActive ? PRIMARY : 'transparent'}; color: ${isActive ? TEXT_INVERSE : TEXT_PRIMARY}; ${i < modes.length - 1 ? `border-right: 1px solid ${BORDER_COLOR};` : ''} `; btn.textContent = t(mode.labelKey); // 悬停效果 btn.addEventListener('mouseenter', () => { if (!isActive) btn.style.background = HOVER_BG; }); btn.addEventListener('mouseleave', () => { if (!isActive) btn.style.background = 'transparent'; }); btn.addEventListener('click', () => { this.registry.engine3d?.setRenderMode(mode.key); // 重建弹窗以刷新状态 this.hide(); this.show(); }); group.appendChild(btn); } section.appendChild(group); return section; } // ======================== 2. 边线开关 ======================== /** 创建边线开关行 */ private createEdgeLineSection(): HTMLElement { const row = document.createElement('div'); row.style.cssText = ROW_STYLE; // 标签 const label = document.createElement('span'); label.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #475569);'; label.textContent = t('setting.edgeLine'); row.appendChild(label); // 开关容器 —— 关闭态使用 --bim-border-strong 保证浅色主题可见 const toggleOffBg = 'var(--bim-border-strong, #cbd5e1)'; const toggleOnBg = PRIMARY; const toggle = document.createElement('div'); toggle.style.cssText = ` width: 40px; height: 22px; border-radius: 11px; cursor: pointer; position: relative; transition: background 0.2s; background: ${this.edgeLineEnabled ? toggleOnBg : toggleOffBg}; `; // 滑块圆点 const knob = document.createElement('div'); knob.style.cssText = ` width: 18px; height: 18px; border-radius: 50%; background: #fff; position: absolute; top: 2px; transition: left 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); left: ${this.edgeLineEnabled ? '20px' : '2px'}; `; toggle.appendChild(knob); toggle.addEventListener('click', () => { this.edgeLineEnabled = !this.edgeLineEnabled; toggle.style.background = this.edgeLineEnabled ? toggleOnBg : toggleOffBg; knob.style.left = this.edgeLineEnabled ? '20px' : '2px'; }); row.appendChild(toggle); return row; } // ======================== 3/4/5. 通用滑块 ======================== /** * 创建一个滑块设置区块(标签 + 滑块 + 数值) * @param labelText 标签文本 * @param value 当前值 * @param min 最小值 * @param max 最大值 * @param onChange 值变化回调 */ private createSliderSection( labelText: string, value: number, min: number, max: number, onChange: (val: number) => void ): HTMLElement { const section = document.createElement('div'); section.style.cssText = 'margin-bottom: 4px;'; // 上部:标签 + 数值 const header = document.createElement('div'); header.style.cssText = ROW_STYLE + ' margin-bottom: 6px;'; const label = document.createElement('span'); label.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #475569);'; label.textContent = labelText; header.appendChild(label); const valueDisplay = document.createElement('span'); valueDisplay.style.cssText = SLIDER_VALUE_STYLE; valueDisplay.textContent = String(value); header.appendChild(valueDisplay); section.appendChild(header); // 滑块 —— 轨道使用 --bim-border-strong 保证在浅色/深色下都可见 const slider = document.createElement('input'); slider.type = 'range'; slider.min = String(min); slider.max = String(max); slider.value = String(value); slider.className = 'bim-setting-slider'; slider.style.cssText = ` width: 100%; height: 4px; -webkit-appearance: none; appearance: none; background: var(--bim-border-strong, #cbd5e1); border-radius: 2px; outline: none; cursor: pointer; `; slider.addEventListener('input', () => { const v = Number(slider.value); valueDisplay.textContent = String(v); onChange(v); }); section.appendChild(slider); return section; } /** * 注入全局滑块拇指样式(只注入一次) * 使用 ::-webkit-slider-thumb / ::-moz-range-thumb 伪元素 */ private ensureSliderStyle(): void { if (document.querySelector('#bim-setting-slider-style')) return; const style = document.createElement('style'); style.id = 'bim-setting-slider-style'; style.textContent = ` .bim-setting-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--bim-primary, #3b82f6); cursor: pointer; border: 2px solid var(--bim-bg-elevated, #fff); box-shadow: 0 1px 3px rgba(0,0,0,0.2); } .bim-setting-slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--bim-primary, #3b82f6); cursor: pointer; border: 2px solid var(--bim-bg-elevated, #fff); box-shadow: 0 1px 3px rgba(0,0,0,0.2); } .bim-setting-slider::-moz-range-track { background: var(--bim-border-strong, #cbd5e1); height: 4px; border-radius: 2px; border: none; } `; document.head.appendChild(style); } // ======================== 6. 环境背景(下拉) ======================== /** 创建环境背景下拉选择区 */ private createEnvironmentSection(): HTMLElement { const section = document.createElement('div'); const label = document.createElement('div'); label.style.cssText = SECTION_LABEL_STYLE; label.textContent = t('setting.environment'); section.appendChild(label); const envKeys: EnvironmentType[] = ['default', 'outdoor', 'indoor', 'night', 'overcast', 'studio']; const select = this.createSelect( envKeys.map(key => ({ value: key, label: t(`setting.environments.${key}` as any) })), this.environmentType, (val) => { this.environmentType = val as EnvironmentType; } ); section.appendChild(select); return section; } // ======================== 7. 显示地面(下拉 + 大小) ======================== /** 创建地面设置区 */ private createGroundSection(): HTMLElement { const section = document.createElement('div'); // 地面类型标签 + 下拉 const label = document.createElement('div'); label.style.cssText = SECTION_LABEL_STYLE; label.textContent = t('setting.ground'); section.appendChild(label); const groundKeys: GroundType[] = ['none', 'concrete', 'grass', 'tile', 'water', 'wood']; const select = this.createSelect( groundKeys.map(key => ({ value: key, label: t(`setting.groundTypes.${key}` as any) })), this.groundType, (val) => { this.groundType = val as GroundType; // 显示/隐藏大小输入 sizeRow.style.display = val === 'none' ? 'none' : 'flex'; } ); section.appendChild(select); // 大小输入行 const sizeRow = document.createElement('div'); sizeRow.style.cssText = ROW_STYLE + ' margin-top: 10px;'; sizeRow.style.display = this.groundType === 'none' ? 'none' : 'flex'; const sizeLabel = document.createElement('span'); sizeLabel.style.cssText = 'font-size: 13px; color: var(--bim-text-secondary, #475569);'; sizeLabel.textContent = t('setting.groundSize'); sizeRow.appendChild(sizeLabel); const sizeInput = document.createElement('input'); sizeInput.type = 'number'; sizeInput.value = String(this.groundSize); sizeInput.min = '1'; sizeInput.max = '10000'; sizeInput.style.cssText = ` width: 80px; padding: 4px 8px; border-radius: 4px; border: 1px solid ${BORDER_COLOR}; background: ${INPUT_BG}; color: ${TEXT_PRIMARY}; font-size: 13px; outline: none; text-align: right; `; sizeInput.addEventListener('input', () => { const v = Number(sizeInput.value); if (!isNaN(v) && v > 0) { this.groundSize = v; } }); // 聚焦时高亮边框 sizeInput.addEventListener('focus', () => { sizeInput.style.borderColor = PRIMARY; }); sizeInput.addEventListener('blur', () => { sizeInput.style.borderColor = BORDER_COLOR; }); sizeRow.appendChild(sizeInput); section.appendChild(sizeRow); return section; } // ======================== 通用下拉框 ======================== /** * 创建一个自定义样式的