新增 亚克力透明度调节功能:支持在浅色主题下调整背景模糊效果强度,范围0-100%

This commit is contained in:
2026-04-18 21:51:30 +08:00
parent 20b065af5b
commit ac87c18e59
6 changed files with 183 additions and 51 deletions

View File

@@ -74,6 +74,7 @@ const settings = ref({
cna: '', cna: '',
currentApiProfile: 'default', currentApiProfile: 'default',
apiProfiles: { default: {} }, apiProfiles: { default: {} },
acrylicIntensity: 50,
}) })
const originalSettings = ref({}) const originalSettings = ref({})
@@ -272,6 +273,7 @@ const loadSettings = async () => {
if (data.cna === undefined) data.cna = '' if (data.cna === undefined) data.cna = ''
if (!data.apiProfiles) data.apiProfiles = { default: {} } if (!data.apiProfiles) data.apiProfiles = { default: {} }
if (!data.currentApiProfile) data.currentApiProfile = 'default' if (!data.currentApiProfile) data.currentApiProfile = 'default'
if (data.acrylicIntensity === undefined) data.acrylicIntensity = 50
settings.value = data settings.value = data
originalSettings.value = JSON.parse(JSON.stringify(data)) originalSettings.value = JSON.parse(JSON.stringify(data))
modified.value = false modified.value = false
@@ -312,6 +314,31 @@ const themeClass = computed(() => {
return '' return ''
}) })
const acrylicStyle = computed(() => {
const intensity = settings.value.acrylicIntensity
if (intensity === undefined || intensity === null) return {}
const opacity = 1 - intensity / 100
const isDark = settings.value.theme === 'Dark'
if (isDark) {
return {
'--bg-primary': `rgba(31, 31, 31, ${Math.max(0.05, opacity * 0.85)})`,
'--bg-secondary': `rgba(45, 45, 45, ${Math.max(0.05, opacity * 0.7)})`,
'--bg-elevated': `rgba(51, 51, 51, ${Math.max(0.05, opacity * 0.95)})`,
'--bg-mica': `rgba(31, 31, 31, ${Math.max(0.05, opacity * 0.85)})`,
'--control-fill': `rgba(51, 51, 51, ${Math.max(0.05, opacity * 0.85)})`,
}
} else {
return {
'--bg-primary': `rgba(243, 243, 243, ${Math.max(0.05, opacity * 0.85)})`,
'--bg-secondary': `rgba(255, 255, 255, ${Math.max(0.05, opacity * 0.7)})`,
'--bg-elevated': `rgba(255, 255, 255, ${Math.max(0.05, opacity * 0.95)})`,
'--bg-mica': `rgba(243, 243, 243, ${Math.max(0.05, opacity * 0.473)})`,
'--control-fill': `rgba(249, 249, 249, ${Math.max(0.05, opacity * 0.85)})`,
}
}
})
const selectServer = name => { const selectServer = name => {
currentServerName.value = name currentServerName.value = name
openEditServerPanel(name) openEditServerPanel(name)
@@ -454,9 +481,39 @@ watch(
} else { } else {
document.body.classList.remove('dark', 'solarized-dark') document.body.classList.remove('dark', 'solarized-dark')
} }
applyAcrylicStyle()
}, },
) )
watch(
() => settings.value.acrylicIntensity,
() => {
applyAcrylicStyle()
},
)
const applyAcrylicStyle = () => {
const intensity = settings.value.acrylicIntensity
if (intensity === undefined || intensity === null) return
const opacity = 1 - intensity / 100
const isDark = settings.value.theme === 'Dark'
const root = document.documentElement
if (isDark) {
root.style.setProperty('--bg-primary', `rgba(31, 31, 31, ${Math.max(0.05, opacity * 0.85)})`)
root.style.setProperty('--bg-secondary', `rgba(45, 45, 45, ${Math.max(0.05, opacity * 0.7)})`)
root.style.setProperty('--bg-elevated', `rgba(51, 51, 51, ${Math.max(0.05, opacity * 0.95)})`)
root.style.setProperty('--bg-mica', `rgba(31, 31, 31, ${Math.max(0.05, opacity * 0.85)})`)
root.style.setProperty('--control-fill', `rgba(51, 51, 51, ${Math.max(0.05, opacity * 0.85)})`)
} else {
root.style.setProperty('--bg-primary', `rgba(243, 243, 243, ${Math.max(0.05, opacity * 0.85)})`)
root.style.setProperty('--bg-secondary', `rgba(255, 255, 255, ${Math.max(0.05, opacity * 0.7)})`)
root.style.setProperty('--bg-elevated', `rgba(255, 255, 255, ${Math.max(0.05, opacity * 0.95)})`)
root.style.setProperty('--bg-mica', `rgba(243, 243, 243, ${Math.max(0.05, opacity * 0.473)})`)
root.style.setProperty('--control-fill', `rgba(249, 249, 249, ${Math.max(0.05, opacity * 0.85)})`)
}
}
onMounted(async () => { onMounted(async () => {
await loadApiProfiles() await loadApiProfiles()
await loadSettings() await loadSettings()
@@ -465,6 +522,7 @@ onMounted(async () => {
if (cls) { if (cls) {
document.body.classList.add(cls) document.body.classList.add(cls)
} }
applyAcrylicStyle()
window.electronAPI.onApiProfileSwitched(async profileName => { window.electronAPI.onApiProfileSwitched(async profileName => {
currentApiProfile.value = profileName currentApiProfile.value = profileName
await loadSettings() await loadSettings()

View File

@@ -26,7 +26,10 @@ export default {
bootAnimationNotShown: 'Not Shown', bootAnimationNotShown: 'Not Shown',
checkpointing: 'Checkpointing', checkpointing: 'Checkpointing',
enabled: 'Enabled', enabled: 'Enabled',
disabled: 'Disabled' disabled: 'Disabled',
acrylicEffect: 'Acrylic Effect',
acrylicMin: 'Opaque',
acrylicMax: 'Transparent'
}, },
theme: { theme: {
xcode: 'Xcode', xcode: 'Xcode',

View File

@@ -26,7 +26,10 @@ export default {
bootAnimationNotShown: '未显示', bootAnimationNotShown: '未显示',
checkpointing: '检查点保存', checkpointing: '检查点保存',
enabled: '已启用', enabled: '已启用',
disabled: '已禁用' disabled: '已禁用',
acrylicEffect: '亚克力效果',
acrylicMin: '不透明',
acrylicMax: '透明'
}, },
theme: { theme: {
xcode: 'Xcode', xcode: 'Xcode',

View File

@@ -26,7 +26,10 @@ export default {
bootAnimationNotShown: '未表示', bootAnimationNotShown: '未表示',
checkpointing: 'チェックポイント保存', checkpointing: 'チェックポイント保存',
enabled: '有効', enabled: '有効',
disabled: '無効' disabled: '無効',
acrylicEffect: 'アクリリック効果',
acrylicMin: '不透明',
acrylicMax: '透明'
}, },
theme: { theme: {
xcode: 'Xcode', xcode: 'Xcode',

View File

@@ -136,49 +136,6 @@
--shadow-xl: 0 16px 32px rgba(0, 0, 0, 0.5), 0 8px 16px rgba(0, 0, 0, 0.3); --shadow-xl: 0 16px 32px rgba(0, 0, 0, 0.5), 0 8px 16px rgba(0, 0, 0, 0.3);
} }
// Solarized Dark Mode
.solarized-dark {
--bg-primary: #002b36;
--bg-secondary: #073642;
--bg-tertiary: #094856;
--bg-elevated: #0a4a5c;
--bg-mica: rgba(0, 43, 54, 0.9);
--text-primary: #839496;
--text-secondary: #93a1a1;
--text-tertiary: #586e75;
--text-disabled: #3d5a64;
--accent: #268bd2;
--accent-hover: #2d9cdb;
--accent-pressed: #1a73c0;
--accent-light: rgba(38, 139, 210, 0.15);
--accent-text: #268bd2;
--border: #1d3a47;
--border-light: #0d3a47;
--border-strong: #2d5a6f;
--success: #2aa198;
--success-bg: rgba(42, 161, 152, 0.15);
--danger: #dc322f;
--danger-bg: rgba(220, 50, 47, 0.15);
--warning: #b58900;
--warning-bg: rgba(181, 137, 0, 0.15);
--info: #268bd2;
--info-bg: rgba(38, 139, 210, 0.15);
--control-fill: #073642;
--control-fill-hover: #0a4a5c;
--control-fill-pressed: #0d5a70;
--control-fill-disabled: #053845;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow: 0 4px 8px rgba(0, 0, 0, 0.4), 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.5), 0 4px 8px rgba(0, 0, 0, 0.35);
--shadow-xl: 0 16px 32px rgba(0, 0, 0, 0.6), 0 8px 16px rgba(0, 0, 0, 0.4);
}
// ============================================================================= // =============================================================================
// Animations // Animations
// ============================================================================= // =============================================================================

View File

@@ -21,10 +21,8 @@
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('general.theme') }}</label> <label class="form-label">{{ $t('general.theme') }}</label>
<select class="form-select" v-model="localSettings.theme"> <select class="form-select" v-model="localSettings.theme">
<option value="Xcode">{{ $t('theme.xcode') }}</option>
<option value="Dark">{{ $t('theme.dark') }}</option>
<option value="Light">{{ $t('theme.light') }}</option> <option value="Light">{{ $t('theme.light') }}</option>
<option value="Solarized Dark">{{ $t('theme.solarizedDark') }}</option> <option value="Dark">{{ $t('theme.dark') }}</option>
</select> </select>
</div> </div>
</div> </div>
@@ -50,6 +48,23 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-row" v-if="supportsAcrylic">
<div class="form-group form-group-full">
<label class="form-label">{{ $t('general.acrylicEffect') }}: {{ localSettings.acrylicIntensity }}%</label>
<div class="slider-container">
<input
type="range"
class="form-slider"
min="0"
max="100"
:value="localSettings.acrylicIntensity"
@input="updateSliderStyle"
ref="sliderRef"
/>
<span class="slider-hint">{{ $t('general.acrylicMin') }} {{ $t('general.acrylicMax') }}</span>
</div>
</div>
</div>
</div> </div>
</section> </section>
</template> </template>
@@ -66,12 +81,105 @@ const props = defineProps({
const emit = defineEmits(['update:settings']) const emit = defineEmits(['update:settings'])
import { computed } from 'vue' import { computed, ref, onMounted, watch, nextTick } from 'vue'
const localSettings = computed({ const localSettings = computed({
get: () => props.settings, get: () => props.settings,
set: val => emit('update:settings', val), set: val => emit('update:settings', val),
}) })
const supportsAcrylic = computed(() => {
return typeof document !== 'undefined' && 'backdropFilter' in document.documentElement.style && props.settings.theme !== 'Dark'
})
const sliderRef = ref(null)
const updateSliderStyle = e => {
const value = e.target.value
const percent = ((value - 0) / (100 - 0)) * 100
e.target.style.backgroundSize = `${percent}% 100%`
emit('update:settings', { ...props.settings, acrylicIntensity: Number(value) })
}
onMounted(() => {
if (sliderRef.value) {
const percent = ((props.settings.acrylicIntensity - 0) / (100 - 0)) * 100
sliderRef.value.style.backgroundSize = `${percent}% 100%`
}
})
watch(
() => props.settings.theme,
() => {
nextTick(() => {
if (sliderRef.value && supportsAcrylic.value) {
const percent = ((props.settings.acrylicIntensity - 0) / (100 - 0)) * 100
sliderRef.value.style.backgroundSize = `${percent}% 100%`
}
})
},
)
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
.form-group-full {
grid-column: 1 / -1;
}
.slider-container {
display: flex;
flex-direction: column;
gap: 4px;
}
.form-slider {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 2px;
outline: none;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
background-image: linear-gradient(var(--accent), var(--accent));
background-size: var(--slider-progress, 50%) 100%;
background-repeat: no-repeat;
}
.form-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
border: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
transition: transform 0.1s ease;
}
.form-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
.form-slider::-webkit-slider-thumb:active {
transform: scale(0.95);
}
.form-slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
border: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.slider-hint {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
text-align: right;
}
</style>