mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-03-21 09:13:10 +00:00
Compare commits
9 Commits
e70d46436c
...
41dc2c5251
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41dc2c5251 | ||
|
|
787c95ca18 | ||
|
|
ba6aab2ba2 | ||
|
|
21a63661cd | ||
|
|
aa09a1ffe1 | ||
|
|
9f95ca19cb | ||
|
|
4cd948ff63 | ||
|
|
f1838a891b | ||
|
|
c4b95aede2 |
@ -16,6 +16,7 @@
|
||||
"@microsoft/clarity": "^1.0.2",
|
||||
"@sentry/vue": "^10.36.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@wydev/noise-core": "^0.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"idb": "^8.0.3",
|
||||
"js-base64": "^3.7.8",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -23,6 +23,9 @@ importers:
|
||||
'@vueuse/core':
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
||||
'@wydev/noise-core':
|
||||
specifier: ^0.1.0
|
||||
version: 0.1.0
|
||||
axios:
|
||||
specifier: ^1.13.2
|
||||
version: 1.13.2
|
||||
@ -1469,6 +1472,9 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@wydev/noise-core@0.1.0':
|
||||
resolution: {integrity: sha512-gHPDPasN8Iy3lHjv/3yhev3rpkMUd/o4iIQQKAKwK2Sewqu76dXMxqCsw9gGEf5ZZBf0YhwClsS7lcDlRo0Qxw==}
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@ -5083,6 +5089,10 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.25(typescript@5.9.3)
|
||||
|
||||
'@wydev/noise-core@0.1.0':
|
||||
dependencies:
|
||||
uuid: 13.0.0
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
||||
284
src/components/NoiseMonitorCard.vue
Normal file
284
src/components/NoiseMonitorCard.vue
Normal file
@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<v-card
|
||||
class="noise-monitor-card"
|
||||
elevation="2"
|
||||
border
|
||||
rounded="xl"
|
||||
height="100%"
|
||||
style="cursor: pointer"
|
||||
@click="showDetail = true"
|
||||
>
|
||||
<v-card-text class="pa-5 d-flex flex-column" style="height: 100%">
|
||||
<!-- 顶部标题行 -->
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-icon
|
||||
:color="statusColor"
|
||||
class="mr-2"
|
||||
size="20"
|
||||
>
|
||||
mdi-microphone
|
||||
</v-icon>
|
||||
<span class="text-subtitle-2 font-weight-medium text-medium-emphasis">
|
||||
环境噪音监测
|
||||
</span>
|
||||
<v-spacer />
|
||||
<v-chip
|
||||
:color="statusColor"
|
||||
size="x-small"
|
||||
variant="tonal"
|
||||
label
|
||||
>
|
||||
{{ statusLabel }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- 分贝显示 -->
|
||||
<div class="noise-db-display d-flex align-center justify-center flex-grow-1">
|
||||
<div class="text-center">
|
||||
<div class="d-flex align-end justify-center">
|
||||
<span
|
||||
class="noise-db-value font-weight-bold"
|
||||
:style="{ color: `rgb(var(--v-theme-${dbColor}))`, fontSize: dbFontSize }"
|
||||
>
|
||||
{{ currentDb }}
|
||||
</span>
|
||||
<span class="text-caption text-medium-emphasis ml-1 mb-1">dB</span>
|
||||
</div>
|
||||
<div class="noise-level-label text-caption mt-1" :class="`text-${dbColor}`">
|
||||
{{ noiseLevel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部迷你波形 + 分数 -->
|
||||
<div class="d-flex align-center mt-2">
|
||||
<!-- 迷你波形条 -->
|
||||
<div class="noise-mini-bars d-flex align-end" style="height: 20px; gap: 2px; flex: 1;">
|
||||
<div
|
||||
v-for="(val, i) in miniBarValues"
|
||||
:key="i"
|
||||
class="noise-mini-bar"
|
||||
:style="{
|
||||
height: `${val}%`,
|
||||
backgroundColor: `rgb(var(--v-theme-${barColor(val)}))`,
|
||||
flex: 1,
|
||||
borderRadius: '2px',
|
||||
transition: 'height 0.15s ease',
|
||||
minWidth: '3px',
|
||||
maxWidth: '6px',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 当前评分 -->
|
||||
<div v-if="currentScore !== null" class="ml-3 text-center">
|
||||
<div
|
||||
class="font-weight-bold text-subtitle-1"
|
||||
:class="`text-${scoreColor}`"
|
||||
>
|
||||
{{ currentScore }}
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis" style="font-size: 10px; line-height: 1;">
|
||||
评分
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<noise-monitor-detail
|
||||
v-model="showDetail"
|
||||
:status="status"
|
||||
:current-db="currentDb"
|
||||
:current-dbfs="currentDbfs"
|
||||
:noise-level="noiseLevel"
|
||||
:db-color="dbColor"
|
||||
:current-score="currentScore"
|
||||
:score-detail="scoreDetail"
|
||||
:ring-buffer="ringBuffer"
|
||||
:last-slice="lastSlice"
|
||||
:history="history"
|
||||
:is-monitoring="isMonitoring"
|
||||
@start="startMonitoring"
|
||||
@stop="stopMonitoring"
|
||||
@calibrate="handleCalibrate"
|
||||
@clear-history="handleClearHistory"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { noiseService } from '@wydev/noise-core'
|
||||
|
||||
const NoiseMonitorDetail = defineAsyncComponent(() =>
|
||||
import('@/components/NoiseMonitorDetail.vue')
|
||||
)
|
||||
|
||||
// 最近 N 个采样用于迷你波形
|
||||
const MINI_BAR_COUNT = 16
|
||||
|
||||
export default {
|
||||
name: 'NoiseMonitorCard',
|
||||
components: { NoiseMonitorDetail },
|
||||
data() {
|
||||
return {
|
||||
showDetail: false,
|
||||
isMonitoring: false,
|
||||
status: 'initializing',
|
||||
currentDbfs: -100,
|
||||
currentDisplayDb: 0,
|
||||
currentScore: null,
|
||||
scoreDetail: null,
|
||||
ringBuffer: [],
|
||||
lastSlice: null,
|
||||
history: [],
|
||||
unsubscribe: null,
|
||||
recentDbValues: new Array(MINI_BAR_COUNT).fill(0),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentDb() {
|
||||
if (!this.isMonitoring || this.status !== 'active') return '--'
|
||||
return Math.round(this.currentDisplayDb)
|
||||
},
|
||||
statusColor() {
|
||||
if (this.status === 'active') return 'success'
|
||||
if (this.status === 'paused') return 'warning'
|
||||
if (this.status === 'permission-denied' || this.status === 'error') return 'error'
|
||||
return 'grey'
|
||||
},
|
||||
statusLabel() {
|
||||
const map = {
|
||||
initializing: '就绪',
|
||||
active: '监测中',
|
||||
paused: '已暂停',
|
||||
'permission-denied': '无权限',
|
||||
error: '错误',
|
||||
}
|
||||
return this.isMonitoring ? (map[this.status] || '未知') : '未启动'
|
||||
},
|
||||
dbColor() {
|
||||
const db = typeof this.currentDb === 'number' ? this.currentDb : 0
|
||||
if (db < 40) return 'success'
|
||||
if (db < 55) return 'light-green'
|
||||
if (db < 70) return 'warning'
|
||||
if (db < 85) return 'orange'
|
||||
return 'error'
|
||||
},
|
||||
dbFontSize() {
|
||||
return '2.5rem'
|
||||
},
|
||||
noiseLevel() {
|
||||
const db = typeof this.currentDb === 'number' ? this.currentDb : 0
|
||||
if (!this.isMonitoring || this.status !== 'active') return '等待监测'
|
||||
if (db < 30) return '极其安静'
|
||||
if (db < 40) return '非常安静'
|
||||
if (db < 50) return '安静'
|
||||
if (db < 60) return '正常交谈'
|
||||
if (db < 70) return '较为嘈杂'
|
||||
if (db < 80) return '嘈杂'
|
||||
if (db < 90) return '非常嘈杂'
|
||||
return '极度嘈杂'
|
||||
},
|
||||
scoreColor() {
|
||||
if (this.currentScore === null) return 'grey'
|
||||
if (this.currentScore >= 80) return 'success'
|
||||
if (this.currentScore >= 60) return 'warning'
|
||||
return 'error'
|
||||
},
|
||||
miniBarValues() {
|
||||
return this.recentDbValues.map(db => {
|
||||
// 将 dB 值 (0-100) 映射到 5%-100% 高度
|
||||
return Math.max(5, Math.min(100, db))
|
||||
})
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.history = noiseService.getHistory()
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async startMonitoring() {
|
||||
try {
|
||||
await noiseService.start()
|
||||
this.isMonitoring = true
|
||||
this.unsubscribe = noiseService.subscribe((snapshot) => {
|
||||
this.status = snapshot.status
|
||||
this.currentDbfs = snapshot.currentDbfs
|
||||
this.currentDisplayDb = snapshot.currentDisplayDb
|
||||
this.ringBuffer = snapshot.ringBuffer || []
|
||||
this.lastSlice = snapshot.lastSlice || null
|
||||
this.currentScore = snapshot.currentScore ?? null
|
||||
this.scoreDetail = snapshot.currentScoreDetail ?? null
|
||||
|
||||
// 更新迷你波形数据
|
||||
const dbVal = Math.max(0, Math.min(100, this.currentDisplayDb))
|
||||
this.recentDbValues.push(dbVal)
|
||||
if (this.recentDbValues.length > MINI_BAR_COUNT) {
|
||||
this.recentDbValues.shift()
|
||||
}
|
||||
|
||||
// 更新历史
|
||||
this.history = noiseService.getHistory()
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('噪音监测启动失败:', e)
|
||||
this.status = 'error'
|
||||
}
|
||||
},
|
||||
stopMonitoring() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe()
|
||||
this.unsubscribe = null
|
||||
}
|
||||
noiseService.stop()
|
||||
this.isMonitoring = false
|
||||
this.status = 'initializing'
|
||||
this.recentDbValues = new Array(MINI_BAR_COUNT).fill(0)
|
||||
this.currentScore = null
|
||||
this.scoreDetail = null
|
||||
},
|
||||
handleCalibrate(targetDb) {
|
||||
noiseService.calibrate(targetDb, (success, msg) => {
|
||||
console.log(success ? '校准成功' : `校准失败: ${msg}`)
|
||||
})
|
||||
},
|
||||
handleClearHistory() {
|
||||
noiseService.clearHistory()
|
||||
this.history = []
|
||||
},
|
||||
barColor(val) {
|
||||
if (val < 30) return 'success'
|
||||
if (val < 55) return 'light-green'
|
||||
if (val < 70) return 'warning'
|
||||
if (val < 85) return 'orange'
|
||||
return 'error'
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.noise-monitor-card {
|
||||
overflow: hidden;
|
||||
|
||||
.noise-db-value {
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.noise-level-label {
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.noise-mini-bars {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1461
src/components/NoiseMonitorDetail.vue
Normal file
1461
src/components/NoiseMonitorDetail.vue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -7,10 +7,11 @@
|
||||
color="error"
|
||||
size="large"
|
||||
@click="$emit('upload')"
|
||||
rounded="xl"
|
||||
>
|
||||
上传
|
||||
</v-btn>
|
||||
<v-btn v-else color="success" size="large" @click="$emit('show-sync-message')">
|
||||
<v-btn v-else color="success" size="large" @click="$emit('show-sync-message')" rounded="xl">
|
||||
同步完成
|
||||
</v-btn>
|
||||
<v-btn
|
||||
|
||||
@ -126,6 +126,7 @@
|
||||
border
|
||||
class="glow-track"
|
||||
height="100%"
|
||||
rounded="xl"
|
||||
@click="handleCardClick('dialog', item.key)"
|
||||
@mousemove="handleMouseMove"
|
||||
@touchmove="handleTouchMove"
|
||||
@ -185,6 +186,7 @@
|
||||
v-for="subject in unusedSubjects"
|
||||
:key="subject.name"
|
||||
border
|
||||
rounded="xl"
|
||||
class="empty-subject-card"
|
||||
@click="handleCardClick('dialog', subject.name)"
|
||||
>
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
<div class="card-content">
|
||||
<div>
|
||||
<div class="text-h6 font-weight-bold">请支持我们 Classworks</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</v-card-item>
|
||||
@ -127,14 +127,14 @@
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
append-icon="mdi-link"
|
||||
href="https://zerocat.houlangs.com"
|
||||
href="https://clock.qqhkx.com/"
|
||||
target="_blank"
|
||||
>
|
||||
<v-list-item-title>
|
||||
感谢 ZeroCat 社区的开发者们
|
||||
感谢 沉浸式时钟
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
新一代,开源,编程社区
|
||||
https://clock.qqhkx.com/
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider class="ma-1"></v-divider>
|
||||
|
||||
@ -1,8 +1,35 @@
|
||||
<template>
|
||||
<settings-card border icon="mdi-monitor" title="显示设置">
|
||||
<v-list>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'"/>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon class="mr-3" icon="mdi-theme-light-dark"/>
|
||||
</template>
|
||||
<v-list-item-title>主题模式</v-list-item-title>
|
||||
<v-list-item-subtitle>选择明亮或暗黑主题</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-btn-toggle
|
||||
v-model="localTheme"
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
>
|
||||
<v-btn value="light">
|
||||
<v-icon class="mr-2" icon="mdi-white-balance-sunny"/>
|
||||
明亮
|
||||
</v-btn>
|
||||
<v-btn value="dark">
|
||||
<v-icon class="mr-2" icon="mdi-moon-waning-crescent"/>
|
||||
暗黑
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'timeCard.enabled'"/>
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'"/>
|
||||
|
||||
<v-divider class="my-2"/>
|
||||
<setting-item :setting-key="'display.dynamicSort'"/>
|
||||
@ -41,15 +68,29 @@
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
import {getSetting, setSetting} from '@/utils/settings';
|
||||
import {useTheme} from 'vuetify';
|
||||
|
||||
export default {
|
||||
name: 'DisplaySettingsCard',
|
||||
components: {SettingsCard, SettingItem},
|
||||
data() {
|
||||
|
||||
return {};
|
||||
setup() {
|
||||
const theme = useTheme();
|
||||
return {theme};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
localTheme: getSetting('theme.mode'),
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
localTheme(newValue) {
|
||||
setSetting('theme.mode', newValue);
|
||||
this.theme.global.name.value = newValue;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
284
src/components/settings/cards/NoiseSettingsCard.vue
Normal file
284
src/components/settings/cards/NoiseSettingsCard.vue
Normal file
@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<settings-card
|
||||
border
|
||||
icon="mdi-microphone"
|
||||
title="噪音监测"
|
||||
>
|
||||
<v-list>
|
||||
<setting-item :setting-key="'noiseMonitor.enabled'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'noiseMonitor.autoStart'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'noiseMonitor.permissionDismissed'" />
|
||||
</v-list>
|
||||
|
||||
<v-divider class="mb-4" />
|
||||
|
||||
<!-- 晚自习时间段配置 -->
|
||||
<div class="px-4 pb-4">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
color="teal"
|
||||
>
|
||||
mdi-clock-edit-outline
|
||||
</v-icon>
|
||||
<span class="text-subtitle-1 font-weight-bold">晚自习时间段</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="addSession"
|
||||
>
|
||||
添加时段
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="text-caption text-medium-emphasis mb-4">
|
||||
配置晚自习时间段后,系统会在对应时段内自动开启噪音监测并记录统计报告。时间段外不会长期记录。
|
||||
</div>
|
||||
|
||||
<v-skeleton-loader
|
||||
v-if="sessionLoading"
|
||||
type="card"
|
||||
class="mb-4"
|
||||
/>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(session, idx) in editSessions"
|
||||
:key="idx"
|
||||
class="mb-3"
|
||||
>
|
||||
<v-card
|
||||
variant="outlined"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-text class="pa-4">
|
||||
<div class="d-flex align-center ga-3 flex-wrap">
|
||||
<v-text-field
|
||||
v-model="session.name"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
label="名称"
|
||||
hide-details
|
||||
style="max-width: 160px;"
|
||||
/>
|
||||
<v-menu
|
||||
v-model="timePickerMenus[idx]"
|
||||
:close-on-content-click="false"
|
||||
location="bottom"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-text-field
|
||||
v-bind="menuProps"
|
||||
:model-value="session.start"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
label="开始时间"
|
||||
readonly
|
||||
hide-details
|
||||
prepend-inner-icon="mdi-clock-outline"
|
||||
style="max-width: 170px;"
|
||||
/>
|
||||
</template>
|
||||
<v-time-picker
|
||||
v-model="session.start"
|
||||
color="primary"
|
||||
format="24hr"
|
||||
scrollable
|
||||
@update:model-value="timePickerMenus[idx] = false"
|
||||
/>
|
||||
</v-menu>
|
||||
<v-text-field
|
||||
v-model.number="session.duration"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
type="number"
|
||||
label="时长"
|
||||
suffix="分钟"
|
||||
hide-details
|
||||
style="max-width: 130px;"
|
||||
:min="10"
|
||||
:max="300"
|
||||
/>
|
||||
<span class="text-caption text-medium-emphasis">
|
||||
至 {{ sessionEndTime(session) }}
|
||||
</span>
|
||||
<v-switch
|
||||
v-model="session.enabled"
|
||||
density="compact"
|
||||
color="primary"
|
||||
hide-details
|
||||
label="启用"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
color="error"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="editSessions.splice(idx, 1)"
|
||||
/>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="editSessions.length === 0"
|
||||
class="text-center text-medium-emphasis py-4"
|
||||
>
|
||||
<v-icon class="mb-1">
|
||||
mdi-clock-outline
|
||||
</v-icon>
|
||||
<div class="text-caption">
|
||||
暂无时间段,点击上方「添加时段」创建
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-divider class="my-5" />
|
||||
|
||||
<!-- 监测参数 -->
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-icon
|
||||
class="mr-2"
|
||||
color="orange"
|
||||
>
|
||||
mdi-alert-decagram
|
||||
</v-icon>
|
||||
<span class="text-subtitle-1 font-weight-bold">监测参数</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center flex-wrap ga-4 mb-4">
|
||||
<v-text-field
|
||||
v-model.number="editAlertThreshold"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
type="number"
|
||||
label="噪音报警阈值"
|
||||
suffix="dB"
|
||||
hide-details
|
||||
style="max-width: 200px;"
|
||||
:min="30"
|
||||
:max="90"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-end ga-3 mb-2">
|
||||
<v-btn
|
||||
variant="text"
|
||||
prepend-icon="mdi-restore"
|
||||
@click="resetSessionConfig"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-content-save"
|
||||
:loading="sessionSaving"
|
||||
@click="saveSessionConfig"
|
||||
>
|
||||
保存配置
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
import dataProvider from '@/utils/dataProvider';
|
||||
|
||||
const DEFAULT_SESSION_CONFIG = {
|
||||
sessions: [
|
||||
{ name: '第1节晚自习', start: '19:20', duration: 70, enabled: true },
|
||||
{ name: '第2节晚自习', start: '20:20', duration: 110, enabled: true },
|
||||
],
|
||||
alertThresholdDb: 55,
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'NoiseSettingsCard',
|
||||
components: { SettingsCard, SettingItem },
|
||||
|
||||
data() {
|
||||
return {
|
||||
// 自习配置
|
||||
sessionLoading: true,
|
||||
sessionSaving: false,
|
||||
editSessions: [],
|
||||
editAlertThreshold: 55,
|
||||
timePickerMenus: {},
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadSessionConfig();
|
||||
},
|
||||
|
||||
methods: {
|
||||
// ===== 自习配置 =====
|
||||
async loadSessionConfig() {
|
||||
this.sessionLoading = true;
|
||||
try {
|
||||
const res = await dataProvider.loadData('noise-session-config');
|
||||
const data = res?.data || res;
|
||||
if (data && data.sessions) {
|
||||
this.editSessions = JSON.parse(JSON.stringify(data.sessions));
|
||||
this.editAlertThreshold = data.alertThresholdDb || 55;
|
||||
} else {
|
||||
this.resetSessionConfig();
|
||||
}
|
||||
} catch {
|
||||
this.resetSessionConfig();
|
||||
} finally {
|
||||
this.sessionLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async saveSessionConfig() {
|
||||
this.sessionSaving = true;
|
||||
try {
|
||||
const config = {
|
||||
sessions: this.editSessions,
|
||||
alertThresholdDb: this.editAlertThreshold,
|
||||
};
|
||||
await dataProvider.saveData('noise-session-config', config);
|
||||
} catch (e) {
|
||||
console.error('保存自习配置失败:', e);
|
||||
} finally {
|
||||
this.sessionSaving = false;
|
||||
}
|
||||
},
|
||||
|
||||
resetSessionConfig() {
|
||||
this.editSessions = JSON.parse(JSON.stringify(DEFAULT_SESSION_CONFIG.sessions));
|
||||
this.editAlertThreshold = DEFAULT_SESSION_CONFIG.alertThresholdDb;
|
||||
},
|
||||
|
||||
addSession() {
|
||||
this.editSessions.push({
|
||||
name: `第${this.editSessions.length + 1}节晚自习`,
|
||||
start: '19:00',
|
||||
duration: 70,
|
||||
enabled: true,
|
||||
});
|
||||
},
|
||||
|
||||
sessionEndTime(session) {
|
||||
if (!session?.start || !session?.duration) return '--:--';
|
||||
const [h, m] = session.start.split(':').map(Number);
|
||||
const totalMin = h * 60 + m + (session.duration || 0);
|
||||
const eh = Math.floor(totalMin / 60) % 24;
|
||||
const em = totalMin % 60;
|
||||
return `${String(eh).padStart(2, '0')}:${String(em).padStart(2, '0')}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -174,12 +174,8 @@
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="theme">
|
||||
<theme-settings-card
|
||||
:loading="loading.theme"
|
||||
border
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
<v-tabs-window-item value="noise">
|
||||
<noise-settings-card border />
|
||||
</v-tabs-window-item>
|
||||
|
||||
<v-tabs-window-item value="notification">
|
||||
@ -262,7 +258,6 @@ import EditSettingsCard from "@/components/settings/cards/EditSettingsCard.vue";
|
||||
import RefreshSettingsCard from "@/components/settings/cards/RefreshSettingsCard.vue";
|
||||
import DisplaySettingsCard from "@/components/settings/cards/DisplaySettingsCard.vue";
|
||||
import DataProviderSettingsCard from "@/components/settings/cards/DataProviderSettingsCard.vue";
|
||||
import ThemeSettingsCard from "@/components/settings/cards/ThemeSettingsCard.vue";
|
||||
import EchoChamberCard from "@/components/settings/cards/EchoChamberCard.vue";
|
||||
import {
|
||||
getSetting,
|
||||
@ -284,6 +279,7 @@ import SubjectManagementCard from "@/components/settings/cards/SubjectManagement
|
||||
import KvDatabaseCard from "@/components/settings/cards/KvDatabaseCard.vue";
|
||||
import HitokotoSettings from "@/components/HitokotoSettings.vue";
|
||||
import NotificationSoundSettings from "@/components/settings/NotificationSoundSettings.vue";
|
||||
import NoiseSettingsCard from "@/components/settings/cards/NoiseSettingsCard.vue";
|
||||
|
||||
export default {
|
||||
name: "Settings",
|
||||
@ -298,7 +294,6 @@ export default {
|
||||
TeacherListCard,
|
||||
AboutCard,
|
||||
DataProviderSettingsCard,
|
||||
ThemeSettingsCard,
|
||||
EchoChamberCard,
|
||||
SettingsExplorer,
|
||||
SettingsLinkGenerator,
|
||||
@ -308,6 +303,7 @@ export default {
|
||||
KvDatabaseCard,
|
||||
HitokotoSettings,
|
||||
NotificationSoundSettings,
|
||||
NoiseSettingsCard,
|
||||
},
|
||||
setup() {
|
||||
const {mobile} = useDisplay();
|
||||
@ -425,9 +421,9 @@ export default {
|
||||
value: "display",
|
||||
},
|
||||
{
|
||||
title: "主题",
|
||||
icon: "mdi-theme-light-dark",
|
||||
value: "theme",
|
||||
title: "噪音监测",
|
||||
icon: "mdi-microphone",
|
||||
value: "noise",
|
||||
},
|
||||
{
|
||||
title: "通知铃声",
|
||||
|
||||
@ -100,6 +100,26 @@ const settingsDefinitions = {
|
||||
icon: "mdi-card-outline",
|
||||
},
|
||||
|
||||
// 噪音监测设置
|
||||
"noiseMonitor.enabled": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "启用环境噪音监测",
|
||||
icon: "mdi-microphone",
|
||||
},
|
||||
"noiseMonitor.autoStart": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "打开页面时自动开始监测",
|
||||
icon: "mdi-play-circle-outline",
|
||||
},
|
||||
"noiseMonitor.permissionDismissed": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "已跳过麦克风权限引导(不再弹出介绍弹框)",
|
||||
icon: "mdi-microphone-off",
|
||||
},
|
||||
|
||||
// 时间卡片设置
|
||||
"timeCard.enabled": {
|
||||
type: "boolean",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user