Skip to content
23 changes: 23 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ export default [
...pluginVue.configs['flat/recommended'],

{
languageOptions: {
globals: {
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
location: 'readonly',
localStorage: 'readonly',
URL: 'readonly',
URLSearchParams: 'readonly',
CSS: 'readonly',
Notification: 'readonly',
requestAnimationFrame: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
console: 'readonly',
require: 'readonly',
self: 'readonly',
caches: 'readonly',
Blob: 'readonly',
},
},
rules: {
'vue/multi-word-component-names': 'off',
},
Expand Down
9 changes: 9 additions & 0 deletions src/components/settings/cards/DisplaySettingsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@
<v-divider class="my-2"/>
<setting-item :setting-key="'display.enhancedTouchMode'"/>

<v-divider class="my-2"/>
<setting-item :setting-key="'display.backgroundImage'"/>

<v-divider class="my-2"/>
<setting-item :setting-key="'display.backgroundBlur'"/>

<v-divider class="my-2"/>
<setting-item :setting-key="'display.backgroundDim'"/>

<v-divider class="my-2"/>
<setting-item :setting-key="'display.showQuickTools'"/>

Expand Down
101 changes: 101 additions & 0 deletions src/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
<template>
<div
v-if="hasBackgroundImage"
class="home-background"
:style="backgroundImageStyle"
aria-hidden="true"
></div>
<div
v-if="hasBackgroundImage"
class="home-background-overlay"
:style="backgroundOverlayStyle"
aria-hidden="true"
></div>
<v-app-bar class="no-select">
<v-app-bar-title>
{{ titleText }}
Expand Down Expand Up @@ -657,6 +669,48 @@ export default {
return `${deviceName} - ${currentDateStr}的作业`;
}
},
hasBackgroundImage() {
return !!this.backgroundImageUrl;
},
backgroundImageUrl() {
void this.settingsTick;
return (getSetting("display.backgroundImage") || "").trim();
},
backgroundBlurAmount() {
void this.settingsTick;
const value = Number(getSetting("display.backgroundBlur"));
return Number.isFinite(value) ? value : 0;
},
backgroundDimAmount() {
void this.settingsTick;
const value = Number(getSetting("display.backgroundDim"));
return Number.isFinite(value) ? value : 0;
},
backgroundImageStyle() {
const url = this.backgroundImageUrl;
if (!this.isSafeBackgroundUrl(url)) return { display: "none" };
const safeUrl = this.sanitizeBackgroundUrl(url);
if (!safeUrl) return { display: "none" };

const blur = Math.min(Math.max(this.backgroundBlurAmount, 0), 50);
const escaped = this.cssEscape(safeUrl);
return {
backgroundImage: `url("${escaped}")`,
filter: `blur(${blur}px)`,
};
},
backgroundOverlayStyle() {
if (!this.hasBackgroundImage) return { display: "none" };

const dim = Math.min(Math.max(this.backgroundDimAmount, 0), 90);
// Slightly reduce overlay blur to avoid overwhelming foreground
const overlayBlur =
Math.min(Math.max(this.backgroundBlurAmount, 0), 50) / 3;
return {
backgroundColor: `rgba(0, 0, 0, ${dim / 100})`,
backdropFilter: `blur(${overlayBlur}px)`,
};
},
sortedItems() {
const items = [];

Expand Down Expand Up @@ -2156,6 +2210,36 @@ export default {

return nameMap[lastPart] || lastPart;
},
isSafeBackgroundUrl(url) {
if (!url) return false;
const trimmed = url.trim();
if (/^(javascript|data|vbscript):/i.test(trimmed)) return false;

try {
const parsed = new URL(trimmed, window.location.origin);
const protocol = parsed.protocol.replace(":", "");
if (!["http", "https"].includes(protocol)) return false;
if (parsed.pathname.includes("..")) return false;
return true;
} catch (e) {
return false;
}
},
sanitizeBackgroundUrl(url) {
if (!this.isSafeBackgroundUrl(url)) return "";
try {
const parsed = new URL(url, window.location.origin);
return parsed.href;
} catch (e) {
return "";
}
},
cssEscape(value) {
if (typeof CSS !== "undefined" && CSS.escape) {
return CSS.escape(value);
}
return encodeURI(value);
},

safeBase64Decode(base64String) {
try {
Expand Down Expand Up @@ -2353,3 +2437,20 @@ export default {
},
};
</script>

<style scoped>
.home-background,
.home-background-overlay {
position: fixed;
inset: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
pointer-events: none;
transition: opacity 0.3s ease, filter 0.3s ease, background-color 0.3s ease;
}

.home-background {
transform: scale(1.02); /* slight zoom to mask blur edges */
}
</style>
2 changes: 0 additions & 2 deletions src/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,6 @@ export default {
return {isMobile: mobile};
},
data() {
const provider = getSetting("server.provider");

const settings = {
server: {
domain: getSetting("server.domain"),
Expand Down
4 changes: 2 additions & 2 deletions src/pages/socket-debugger.vue
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@

<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
import { getSocket, getServerUrl, disconnect, on, off } from '@/utils/socketClient';
import { getSocket, getServerUrl, disconnect } from '@/utils/socketClient';

// 状态数据
const serverUrl = ref(getServerUrl());
Expand Down Expand Up @@ -582,7 +582,7 @@ function cleanupSocketListeners() {
if (socket.io?.engine) {
socket.io.engine.off('upgrade', onUpgrade);
}
} catch (e) {
} catch {
// 忽略清理错误
}

Expand Down
20 changes: 20 additions & 0 deletions src/utils/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,26 @@ const settingsDefinitions = {
description: "是否启用增强触摸模式",
icon: "mdi-gesture-tap-button",
},
"display.backgroundImage": {
type: "string",
default: "",
description: "主页背景图地址",
icon: "mdi-image-outline",
},
"display.backgroundBlur": {
type: "number",
default: 12,
validate: (value) => value >= 0 && value <= 50,
description: "背景模糊强度",
icon: "mdi-blur",
},
"display.backgroundDim": {
type: "number",
default: 35,
validate: (value) => value >= 0 && value <= 90,
description: "背景暗色程度(%)",
icon: "mdi-weather-night",
},
"display.showAntiScreenBurnCard": {
type: "boolean",
default: false,
Expand Down