mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-05-13 19:35:07 +00:00
feat: add custom background image with frosted glass effect
- Add background settings (enabled, url, imageData, blur, opacity) to settings.js - Add watchSettings same-tab reactivity via custom DOM event dispatch - Create BackgroundSettingsCard.vue with URL input, file upload, sliders, preview - Apply background layer in App.vue with blur filter and dark overlay - Add background settings tab to settings.vue navigation Agent-Logs-Url: https://github.com/ZeroCatDev/Classworks/sessions/6dfae4c0-df49-4612-88b8-9e31287538b0 Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
This commit is contained in:
parent
f842429f9b
commit
3f6e0b88bd
76
src/App.vue
76
src/App.vue
@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app :style="vAppStyle">
|
||||||
|
<!-- 自定义背景层 -->
|
||||||
|
<template v-if="bgEnabled">
|
||||||
|
<div class="app-background-image" :style="bgImageStyle" />
|
||||||
|
<div class="app-background-overlay" :style="bgOverlayStyle" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 正常路由 -->
|
<!-- 正常路由 -->
|
||||||
<router-view v-slot="{ Component, route }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<transition mode="out-in" name="md3">
|
<transition mode="out-in" name="md3">
|
||||||
@ -12,24 +18,71 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { useTheme } from "vuetify";
|
import { useTheme } from "vuetify";
|
||||||
import { getSetting } from "@/utils/settings";
|
import { getSetting, watchSettings } from "@/utils/settings";
|
||||||
import RateLimitModal from "@/components/RateLimitModal.vue";
|
import RateLimitModal from "@/components/RateLimitModal.vue";
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
// Background reactive refs
|
||||||
|
const bgEnabled = ref(false);
|
||||||
|
const bgSrc = ref("");
|
||||||
|
const bgBlur = ref(10);
|
||||||
|
const bgOpacity = ref(30);
|
||||||
|
|
||||||
|
function loadBgSettings() {
|
||||||
|
bgEnabled.value = getSetting("background.enabled") || false;
|
||||||
|
const imageData = getSetting("background.imageData") || "";
|
||||||
|
const url = getSetting("background.url") || "";
|
||||||
|
bgSrc.value = imageData || url;
|
||||||
|
bgBlur.value = getSetting("background.blur") ?? 10;
|
||||||
|
bgOpacity.value = getSetting("background.opacity") ?? 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vAppStyle = computed(() => {
|
||||||
|
if (!bgEnabled.value || !bgSrc.value) return {};
|
||||||
|
return { background: "transparent" };
|
||||||
|
});
|
||||||
|
|
||||||
|
const bgImageStyle = computed(() => ({
|
||||||
|
backgroundImage: `url(${bgSrc.value})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
filter: `blur(${bgBlur.value}px)`,
|
||||||
|
// Scale slightly to hide blur edge artifacts
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bgOverlayStyle = computed(() => ({
|
||||||
|
background: `rgba(0, 0, 0, ${bgOpacity.value / 100})`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let unwatchSettings = null;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 应用保存的主题设置
|
// 应用保存的主题设置
|
||||||
const savedTheme = getSetting("theme.mode");
|
const savedTheme = getSetting("theme.mode");
|
||||||
theme.global.name.value = savedTheme;
|
theme.global.name.value = savedTheme;
|
||||||
|
|
||||||
|
loadBgSettings();
|
||||||
|
|
||||||
|
unwatchSettings = watchSettings(() => {
|
||||||
|
loadBgSettings();
|
||||||
|
// Reapply theme on settings change too
|
||||||
|
theme.global.name.value = getSetting("theme.mode");
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('beforeinstallprompt', (e) => {
|
window.addEventListener('beforeinstallprompt', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.deferredPwaPrompt = e;
|
window.deferredPwaPrompt = e;
|
||||||
window.dispatchEvent(new Event('pwa-prompt-ready'));
|
window.dispatchEvent(new Event('pwa-prompt-ready'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (unwatchSettings) unwatchSettings();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
/* 全局样式(从 index.vue 迁移,确保全局可用且仅加载一次) */
|
/* 全局样式(从 index.vue 迁移,确保全局可用且仅加载一次) */
|
||||||
@ -52,4 +105,21 @@ onMounted(() => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-0.5vw);
|
transform: translateX(-0.5vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义背景层 */
|
||||||
|
.app-background-image,
|
||||||
|
.app-background-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-background-image {
|
||||||
|
transform-origin: center center;
|
||||||
|
will-change: transform, filter;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
422
src/components/settings/cards/BackgroundSettingsCard.vue
Normal file
422
src/components/settings/cards/BackgroundSettingsCard.vue
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
<template>
|
||||||
|
<settings-card border icon="mdi-image" title="背景设置">
|
||||||
|
<v-list>
|
||||||
|
<setting-item :setting-key="'background.enabled'" />
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-divider class="mb-4" />
|
||||||
|
|
||||||
|
<div class="px-4 pb-4">
|
||||||
|
<!-- 预览区域 -->
|
||||||
|
<div class="preview-area mb-6" :style="previewContainerStyle">
|
||||||
|
<div class="preview-bg" :style="previewBgStyle" />
|
||||||
|
<div class="preview-overlay" :style="previewOverlayStyle" />
|
||||||
|
<div class="preview-text">背景预览</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片来源 -->
|
||||||
|
<div class="d-flex align-center mb-4">
|
||||||
|
<v-icon class="mr-2" color="primary">mdi-image-search</v-icon>
|
||||||
|
<span class="text-subtitle-1 font-weight-bold">图片来源</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 来源选择 -->
|
||||||
|
<v-btn-toggle
|
||||||
|
v-model="imageSource"
|
||||||
|
color="primary"
|
||||||
|
density="comfortable"
|
||||||
|
class="mb-4"
|
||||||
|
mandatory
|
||||||
|
rounded="xl"
|
||||||
|
>
|
||||||
|
<v-btn value="url" prepend-icon="mdi-link-variant">网络地址</v-btn>
|
||||||
|
<v-btn value="upload" prepend-icon="mdi-upload">本地上传</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
|
||||||
|
<!-- URL 输入 -->
|
||||||
|
<div v-if="imageSource === 'url'" class="mb-4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="localUrl"
|
||||||
|
label="图片地址"
|
||||||
|
placeholder="https://example.com/background.jpg"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
prepend-inner-icon="mdi-link"
|
||||||
|
clearable
|
||||||
|
hide-details="auto"
|
||||||
|
:rules="[validateUrl]"
|
||||||
|
@update:model-value="onUrlChange"
|
||||||
|
/>
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||||
|
<v-chip
|
||||||
|
v-for="preset in urlPresets"
|
||||||
|
:key="preset.label"
|
||||||
|
size="small"
|
||||||
|
variant="tonal"
|
||||||
|
color="primary"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="applyPreset(preset.url)"
|
||||||
|
>
|
||||||
|
{{ preset.label }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 本地上传 -->
|
||||||
|
<div v-if="imageSource === 'upload'" class="mb-4">
|
||||||
|
<div
|
||||||
|
class="upload-area rounded-xl pa-6 text-center mb-3"
|
||||||
|
:class="{ 'upload-hover': isDragging }"
|
||||||
|
@dragover.prevent="isDragging = true"
|
||||||
|
@dragleave="isDragging = false"
|
||||||
|
@drop.prevent="handleDrop"
|
||||||
|
@click="triggerFileInput"
|
||||||
|
>
|
||||||
|
<v-icon size="40" color="primary" class="mb-2">mdi-image-plus</v-icon>
|
||||||
|
<div class="text-body-2">点击或拖拽图片到此处上传</div>
|
||||||
|
<div class="text-caption text-medium-emphasis mt-1">支持 JPG、PNG、WebP、GIF(建议小于 2MB)</div>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-if="uploadWarning"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
class="mb-2"
|
||||||
|
icon="mdi-alert"
|
||||||
|
>
|
||||||
|
{{ uploadWarning }}
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<div v-if="localImageData" class="d-flex align-center ga-2">
|
||||||
|
<v-chip color="success" prepend-icon="mdi-check-circle" size="small">
|
||||||
|
已上传本地图片
|
||||||
|
</v-chip>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
prepend-icon="mdi-delete"
|
||||||
|
@click="clearUploadedImage"
|
||||||
|
>
|
||||||
|
清除
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="my-5" />
|
||||||
|
|
||||||
|
<!-- 毛玻璃效果设置 -->
|
||||||
|
<div class="d-flex align-center mb-4">
|
||||||
|
<v-icon class="mr-2" color="blue">mdi-blur</v-icon>
|
||||||
|
<span class="text-subtitle-1 font-weight-bold">毛玻璃效果</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex justify-space-between align-center mb-1">
|
||||||
|
<span class="text-body-2 text-medium-emphasis">模糊幅度</span>
|
||||||
|
<span class="text-body-2 font-weight-bold">{{ localBlur }}px</span>
|
||||||
|
</div>
|
||||||
|
<v-slider
|
||||||
|
v-model="localBlur"
|
||||||
|
:min="0"
|
||||||
|
:max="50"
|
||||||
|
:step="1"
|
||||||
|
color="primary"
|
||||||
|
track-color="grey-lighten-3"
|
||||||
|
thumb-label
|
||||||
|
hide-details
|
||||||
|
@update:model-value="onBlurChange"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon size="small" color="grey">mdi-blur-off</v-icon>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<v-icon size="small" color="primary">mdi-blur</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="d-flex justify-space-between align-center mb-1">
|
||||||
|
<span class="text-body-2 text-medium-emphasis">遮罩暗色程度</span>
|
||||||
|
<span class="text-body-2 font-weight-bold">{{ localOpacity }}%</span>
|
||||||
|
</div>
|
||||||
|
<v-slider
|
||||||
|
v-model="localOpacity"
|
||||||
|
:min="0"
|
||||||
|
:max="80"
|
||||||
|
:step="1"
|
||||||
|
color="blue-grey"
|
||||||
|
track-color="grey-lighten-3"
|
||||||
|
thumb-label
|
||||||
|
hide-details
|
||||||
|
@update:model-value="onOpacityChange"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon size="small" color="grey">mdi-brightness-7</v-icon>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<v-icon size="small" color="blue-grey">mdi-brightness-2</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-slider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="my-5" />
|
||||||
|
|
||||||
|
<!-- 保存按钮 -->
|
||||||
|
<div class="d-flex justify-end ga-3">
|
||||||
|
<v-btn variant="text" prepend-icon="mdi-restore" @click="resetAll">
|
||||||
|
重置
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="elevated"
|
||||||
|
prepend-icon="mdi-content-save"
|
||||||
|
:loading="saving"
|
||||||
|
@click="saveAll"
|
||||||
|
>
|
||||||
|
保存设置
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</settings-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import SettingsCard from '@/components/SettingsCard.vue';
|
||||||
|
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||||
|
import { getSetting, setSetting, resetSetting } from '@/utils/settings';
|
||||||
|
|
||||||
|
const URL_PRESETS = [
|
||||||
|
{ label: 'Bing 每日壁纸', url: 'https://bing.img.run/rand_uhd.php' },
|
||||||
|
{ label: '随机风景', url: 'https://picsum.photos/1920/1080?random=1' },
|
||||||
|
{ label: '随机自然', url: 'https://source.unsplash.com/1920x1080/?nature' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MAX_IMAGE_SIZE_MB = 2;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'BackgroundSettingsCard',
|
||||||
|
components: { SettingsCard, SettingItem },
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const imageData = getSetting('background.imageData') || '';
|
||||||
|
const url = getSetting('background.url') || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageSource: imageData ? 'upload' : 'url',
|
||||||
|
localUrl: url,
|
||||||
|
localImageData: imageData,
|
||||||
|
localBlur: getSetting('background.blur') ?? 10,
|
||||||
|
localOpacity: getSetting('background.opacity') ?? 30,
|
||||||
|
isDragging: false,
|
||||||
|
saving: false,
|
||||||
|
uploadWarning: '',
|
||||||
|
urlPresets: URL_PRESETS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
/** The active image src for preview */
|
||||||
|
activeImageSrc() {
|
||||||
|
if (this.imageSource === 'upload' && this.localImageData) {
|
||||||
|
return this.localImageData;
|
||||||
|
}
|
||||||
|
if (this.imageSource === 'url' && this.localUrl) {
|
||||||
|
return this.localUrl;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
previewContainerStyle() {
|
||||||
|
return {
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: '160px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '1px solid rgba(128,128,128,0.3)',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
previewBgStyle() {
|
||||||
|
if (!this.activeImageSrc) {
|
||||||
|
return {
|
||||||
|
position: 'absolute',
|
||||||
|
inset: '0',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
filter: `blur(${this.localBlur}px)`,
|
||||||
|
transform: 'scale(1.1)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
position: 'absolute',
|
||||||
|
inset: '0',
|
||||||
|
backgroundImage: `url(${this.activeImageSrc})`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
filter: `blur(${this.localBlur}px)`,
|
||||||
|
transform: 'scale(1.1)',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
previewOverlayStyle() {
|
||||||
|
return {
|
||||||
|
position: 'absolute',
|
||||||
|
inset: '0',
|
||||||
|
background: `rgba(0, 0, 0, ${this.localOpacity / 100})`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
validateUrl(val) {
|
||||||
|
if (!val) return true;
|
||||||
|
try {
|
||||||
|
new URL(val);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return '请输入有效的图片地址';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onUrlChange(val) {
|
||||||
|
this.localUrl = val || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
onBlurChange(val) {
|
||||||
|
this.localBlur = val;
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpacityChange(val) {
|
||||||
|
this.localOpacity = val;
|
||||||
|
},
|
||||||
|
|
||||||
|
applyPreset(url) {
|
||||||
|
this.localUrl = url;
|
||||||
|
this.imageSource = 'url';
|
||||||
|
},
|
||||||
|
|
||||||
|
triggerFileInput() {
|
||||||
|
this.$refs.fileInput.click();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDrop(event) {
|
||||||
|
this.isDragging = false;
|
||||||
|
const file = event.dataTransfer?.files?.[0];
|
||||||
|
if (file) this.processFile(file);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFileChange(event) {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) this.processFile(file);
|
||||||
|
// Reset input so same file can be re-selected
|
||||||
|
event.target.value = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
processFile(file) {
|
||||||
|
this.uploadWarning = '';
|
||||||
|
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
this.uploadWarning = '请选择图片文件';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeMB = file.size / 1024 / 1024;
|
||||||
|
if (sizeMB > MAX_IMAGE_SIZE_MB) {
|
||||||
|
this.uploadWarning = `图片大小为 ${sizeMB.toFixed(1)}MB,超过 ${MAX_IMAGE_SIZE_MB}MB 建议大小,可能影响性能`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.localImageData = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearUploadedImage() {
|
||||||
|
this.localImageData = '';
|
||||||
|
this.uploadWarning = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveAll() {
|
||||||
|
this.saving = true;
|
||||||
|
try {
|
||||||
|
// Determine which image source to persist
|
||||||
|
if (this.imageSource === 'upload') {
|
||||||
|
setSetting('background.imageData', this.localImageData || '');
|
||||||
|
setSetting('background.url', '');
|
||||||
|
} else {
|
||||||
|
setSetting('background.url', this.localUrl || '');
|
||||||
|
setSetting('background.imageData', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
setSetting('background.blur', this.localBlur);
|
||||||
|
setSetting('background.opacity', this.localOpacity);
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetAll() {
|
||||||
|
resetSetting('background.enabled');
|
||||||
|
resetSetting('background.url');
|
||||||
|
resetSetting('background.imageData');
|
||||||
|
resetSetting('background.blur');
|
||||||
|
resetSetting('background.opacity');
|
||||||
|
|
||||||
|
this.localUrl = getSetting('background.url') || '';
|
||||||
|
this.localImageData = getSetting('background.imageData') || '';
|
||||||
|
this.localBlur = getSetting('background.blur') ?? 10;
|
||||||
|
this.localOpacity = getSetting('background.opacity') ?? 30;
|
||||||
|
this.imageSource = 'url';
|
||||||
|
this.uploadWarning = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.preview-area {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-text {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
border: 2px dashed rgba(128, 128, 128, 0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
background: rgba(128, 128, 128, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area:hover,
|
||||||
|
.upload-hover {
|
||||||
|
border-color: rgb(var(--v-theme-primary));
|
||||||
|
background: rgba(var(--v-theme-primary), 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-2 {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -195,6 +195,10 @@
|
|||||||
<homework-template-card border/>
|
<homework-template-card border/>
|
||||||
</v-tabs-window-item>
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<v-tabs-window-item value="background">
|
||||||
|
<background-settings-card border />
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
<v-tabs-window-item value="developer"
|
<v-tabs-window-item value="developer"
|
||||||
>
|
>
|
||||||
<settings-card border icon="mdi-developer-board" title="开发者选项">
|
<settings-card border icon="mdi-developer-board" title="开发者选项">
|
||||||
@ -280,6 +284,7 @@ import KvDatabaseCard from "@/components/settings/cards/KvDatabaseCard.vue";
|
|||||||
import HitokotoSettings from "@/components/HitokotoSettings.vue";
|
import HitokotoSettings from "@/components/HitokotoSettings.vue";
|
||||||
import NotificationSoundSettings from "@/components/settings/NotificationSoundSettings.vue";
|
import NotificationSoundSettings from "@/components/settings/NotificationSoundSettings.vue";
|
||||||
import NoiseSettingsCard from "@/components/settings/cards/NoiseSettingsCard.vue";
|
import NoiseSettingsCard from "@/components/settings/cards/NoiseSettingsCard.vue";
|
||||||
|
import BackgroundSettingsCard from "@/components/settings/cards/BackgroundSettingsCard.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
@ -304,6 +309,7 @@ export default {
|
|||||||
HitokotoSettings,
|
HitokotoSettings,
|
||||||
NotificationSoundSettings,
|
NotificationSoundSettings,
|
||||||
NoiseSettingsCard,
|
NoiseSettingsCard,
|
||||||
|
BackgroundSettingsCard,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const {mobile} = useDisplay();
|
const {mobile} = useDisplay();
|
||||||
@ -442,6 +448,12 @@ export default {
|
|||||||
value: "randomPicker",
|
value: "randomPicker",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: "背景",
|
||||||
|
icon: "mdi-image",
|
||||||
|
value: "background",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "开发者",
|
title: "开发者",
|
||||||
icon: "mdi-developer-board",
|
icon: "mdi-developer-board",
|
||||||
|
|||||||
@ -441,6 +441,40 @@ const settingsDefinitions = {
|
|||||||
// 设置应用的主题模式,可选亮色或暗色主题
|
// 设置应用的主题模式,可选亮色或暗色主题
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 背景设置
|
||||||
|
"background.enabled": {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
description: "启用自定义背景",
|
||||||
|
icon: "mdi-image",
|
||||||
|
},
|
||||||
|
"background.url": {
|
||||||
|
type: "string",
|
||||||
|
default: "",
|
||||||
|
description: "背景图片地址",
|
||||||
|
icon: "mdi-link",
|
||||||
|
},
|
||||||
|
"background.imageData": {
|
||||||
|
type: "string",
|
||||||
|
default: "",
|
||||||
|
description: "本地背景图片(Base64)",
|
||||||
|
icon: "mdi-image-area",
|
||||||
|
},
|
||||||
|
"background.blur": {
|
||||||
|
type: "number",
|
||||||
|
default: 10,
|
||||||
|
validate: (value) => value >= 0 && value <= 50,
|
||||||
|
description: "毛玻璃模糊幅度(px)",
|
||||||
|
icon: "mdi-blur",
|
||||||
|
},
|
||||||
|
"background.opacity": {
|
||||||
|
type: "number",
|
||||||
|
default: 30,
|
||||||
|
validate: (value) => value >= 0 && value <= 80,
|
||||||
|
description: "遮罩暗色程度(%)",
|
||||||
|
icon: "mdi-circle-half-full",
|
||||||
|
},
|
||||||
|
|
||||||
// 通知铃声设置
|
// 通知铃声设置
|
||||||
"notification.singleSound": {
|
"notification.singleSound": {
|
||||||
type: "string",
|
type: "string",
|
||||||
@ -674,6 +708,13 @@ class SettingsManagerClass {
|
|||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
this.logSettingsChange(key, oldValue, value);
|
this.logSettingsChange(key, oldValue, value);
|
||||||
|
|
||||||
|
// 触发同标签页内的设置变化事件
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.dispatchEvent(new CustomEvent("classworks-settings-changed", {
|
||||||
|
detail: { key, value },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// 为了保持向后兼容,同时更新旧的localStorage键
|
// 为了保持向后兼容,同时更新旧的localStorage键
|
||||||
const legacyKey = definition.legacyKey;
|
const legacyKey = definition.legacyKey;
|
||||||
if (legacyKey && typeof localStorage !== "undefined") {
|
if (legacyKey && typeof localStorage !== "undefined") {
|
||||||
@ -721,6 +762,13 @@ class SettingsManagerClass {
|
|||||||
|
|
||||||
this.settingsCache[key] = definition.default;
|
this.settingsCache[key] = definition.default;
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
|
|
||||||
|
// 触发同标签页内的设置变化事件
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.dispatchEvent(new CustomEvent("classworks-settings-changed", {
|
||||||
|
detail: { key, value: definition.default },
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -743,15 +791,23 @@ class SettingsManagerClass {
|
|||||||
if (typeof window === "undefined") return () => {
|
if (typeof window === "undefined") return () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = (event) => {
|
const storageHandler = (event) => {
|
||||||
if (event.key === SETTINGS_STORAGE_KEY) {
|
if (event.key === SETTINGS_STORAGE_KEY) {
|
||||||
this.settingsCache = JSON.parse(event.newValue);
|
this.settingsCache = JSON.parse(event.newValue);
|
||||||
callback(this.settingsCache);
|
callback(this.settingsCache);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("storage", handler);
|
const customHandler = () => {
|
||||||
return () => window.removeEventListener("storage", handler);
|
callback(this.settingsCache);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("storage", storageHandler);
|
||||||
|
window.addEventListener("classworks-settings-changed", customHandler);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("storage", storageHandler);
|
||||||
|
window.removeEventListener("classworks-settings-changed", customHandler);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user