mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-03-21 09:13:10 +00:00
Compare commits
No commits in common. "41dc2c525153dc368c23b35c7973f497a72d48dc" and "e70d46436c60d29e12b64c9f8c0d6e9a3158a5b1" have entirely different histories.
41dc2c5251
...
e70d46436c
@ -16,7 +16,6 @@
|
|||||||
"@microsoft/clarity": "^1.0.2",
|
"@microsoft/clarity": "^1.0.2",
|
||||||
"@sentry/vue": "^10.36.0",
|
"@sentry/vue": "^10.36.0",
|
||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@wydev/noise-core": "^0.1.0",
|
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"js-base64": "^3.7.8",
|
"js-base64": "^3.7.8",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -23,9 +23,6 @@ importers:
|
|||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
|
||||||
'@wydev/noise-core':
|
|
||||||
specifier: ^0.1.0
|
|
||||||
version: 0.1.0
|
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.13.2
|
specifier: ^1.13.2
|
||||||
version: 1.13.2
|
version: 1.13.2
|
||||||
@ -1472,9 +1469,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.5.0
|
vue: ^3.5.0
|
||||||
|
|
||||||
'@wydev/noise-core@0.1.0':
|
|
||||||
resolution: {integrity: sha512-gHPDPasN8Iy3lHjv/3yhev3rpkMUd/o4iIQQKAKwK2Sewqu76dXMxqCsw9gGEf5ZZBf0YhwClsS7lcDlRo0Qxw==}
|
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -5089,10 +5083,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.25(typescript@5.9.3)
|
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):
|
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.15.0
|
acorn: 8.15.0
|
||||||
|
|||||||
@ -1,284 +0,0 @@
|
|||||||
<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>
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -7,11 +7,10 @@
|
|||||||
color="error"
|
color="error"
|
||||||
size="large"
|
size="large"
|
||||||
@click="$emit('upload')"
|
@click="$emit('upload')"
|
||||||
rounded="xl"
|
|
||||||
>
|
>
|
||||||
上传
|
上传
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn v-else color="success" size="large" @click="$emit('show-sync-message')" rounded="xl">
|
<v-btn v-else color="success" size="large" @click="$emit('show-sync-message')">
|
||||||
同步完成
|
同步完成
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|||||||
@ -126,7 +126,6 @@
|
|||||||
border
|
border
|
||||||
class="glow-track"
|
class="glow-track"
|
||||||
height="100%"
|
height="100%"
|
||||||
rounded="xl"
|
|
||||||
@click="handleCardClick('dialog', item.key)"
|
@click="handleCardClick('dialog', item.key)"
|
||||||
@mousemove="handleMouseMove"
|
@mousemove="handleMouseMove"
|
||||||
@touchmove="handleTouchMove"
|
@touchmove="handleTouchMove"
|
||||||
@ -186,7 +185,6 @@
|
|||||||
v-for="subject in unusedSubjects"
|
v-for="subject in unusedSubjects"
|
||||||
:key="subject.name"
|
:key="subject.name"
|
||||||
border
|
border
|
||||||
rounded="xl"
|
|
||||||
class="empty-subject-card"
|
class="empty-subject-card"
|
||||||
@click="handleCardClick('dialog', subject.name)"
|
@click="handleCardClick('dialog', subject.name)"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-h6 font-weight-bold">请支持我们 Classworks</div>
|
<div class="text-h6 font-weight-bold">请支持我们 Classworks</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-card-item>
|
</v-card-item>
|
||||||
@ -127,14 +127,14 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
append-icon="mdi-link"
|
append-icon="mdi-link"
|
||||||
href="https://clock.qqhkx.com/"
|
href="https://zerocat.houlangs.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
感谢 沉浸式时钟
|
感谢 ZeroCat 社区的开发者们
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
https://clock.qqhkx.com/
|
新一代,开源,编程社区
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-divider class="ma-1"></v-divider>
|
<v-divider class="ma-1"></v-divider>
|
||||||
|
|||||||
@ -1,36 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<settings-card border icon="mdi-monitor" title="显示设置">
|
<settings-card border icon="mdi-monitor" title="显示设置">
|
||||||
<v-list>
|
<v-list>
|
||||||
<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'"/>
|
<setting-item :setting-key="'display.emptySubjectDisplay'"/>
|
||||||
|
|
||||||
|
|
||||||
<v-divider class="my-2"/>
|
<v-divider class="my-2"/>
|
||||||
<setting-item :setting-key="'display.dynamicSort'"/>
|
<setting-item :setting-key="'display.dynamicSort'"/>
|
||||||
|
|
||||||
@ -68,29 +41,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import SettingsCard from '@/components/SettingsCard.vue';
|
import SettingsCard from '@/components/SettingsCard.vue';
|
||||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||||
import {getSetting, setSetting} from '@/utils/settings';
|
|
||||||
import {useTheme} from 'vuetify';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DisplaySettingsCard',
|
name: 'DisplaySettingsCard',
|
||||||
components: {SettingsCard, SettingItem},
|
components: {SettingsCard, SettingItem},
|
||||||
|
|
||||||
setup() {
|
|
||||||
const theme = useTheme();
|
|
||||||
return {theme};
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
|
||||||
localTheme: getSetting('theme.mode'),
|
return {};
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
localTheme(newValue) {
|
|
||||||
setSetting('theme.mode', newValue);
|
|
||||||
this.theme.global.name.value = newValue;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,284 +0,0 @@
|
|||||||
<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,8 +174,12 @@
|
|||||||
/>
|
/>
|
||||||
</v-tabs-window-item>
|
</v-tabs-window-item>
|
||||||
|
|
||||||
<v-tabs-window-item value="noise">
|
<v-tabs-window-item value="theme">
|
||||||
<noise-settings-card border />
|
<theme-settings-card
|
||||||
|
:loading="loading.theme"
|
||||||
|
border
|
||||||
|
@saved="onSettingsSaved"
|
||||||
|
/>
|
||||||
</v-tabs-window-item>
|
</v-tabs-window-item>
|
||||||
|
|
||||||
<v-tabs-window-item value="notification">
|
<v-tabs-window-item value="notification">
|
||||||
@ -258,6 +262,7 @@ import EditSettingsCard from "@/components/settings/cards/EditSettingsCard.vue";
|
|||||||
import RefreshSettingsCard from "@/components/settings/cards/RefreshSettingsCard.vue";
|
import RefreshSettingsCard from "@/components/settings/cards/RefreshSettingsCard.vue";
|
||||||
import DisplaySettingsCard from "@/components/settings/cards/DisplaySettingsCard.vue";
|
import DisplaySettingsCard from "@/components/settings/cards/DisplaySettingsCard.vue";
|
||||||
import DataProviderSettingsCard from "@/components/settings/cards/DataProviderSettingsCard.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 EchoChamberCard from "@/components/settings/cards/EchoChamberCard.vue";
|
||||||
import {
|
import {
|
||||||
getSetting,
|
getSetting,
|
||||||
@ -279,7 +284,6 @@ import SubjectManagementCard from "@/components/settings/cards/SubjectManagement
|
|||||||
import KvDatabaseCard from "@/components/settings/cards/KvDatabaseCard.vue";
|
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";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
@ -294,6 +298,7 @@ export default {
|
|||||||
TeacherListCard,
|
TeacherListCard,
|
||||||
AboutCard,
|
AboutCard,
|
||||||
DataProviderSettingsCard,
|
DataProviderSettingsCard,
|
||||||
|
ThemeSettingsCard,
|
||||||
EchoChamberCard,
|
EchoChamberCard,
|
||||||
SettingsExplorer,
|
SettingsExplorer,
|
||||||
SettingsLinkGenerator,
|
SettingsLinkGenerator,
|
||||||
@ -303,7 +308,6 @@ export default {
|
|||||||
KvDatabaseCard,
|
KvDatabaseCard,
|
||||||
HitokotoSettings,
|
HitokotoSettings,
|
||||||
NotificationSoundSettings,
|
NotificationSoundSettings,
|
||||||
NoiseSettingsCard,
|
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const {mobile} = useDisplay();
|
const {mobile} = useDisplay();
|
||||||
@ -421,9 +425,9 @@ export default {
|
|||||||
value: "display",
|
value: "display",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "噪音监测",
|
title: "主题",
|
||||||
icon: "mdi-microphone",
|
icon: "mdi-theme-light-dark",
|
||||||
value: "noise",
|
value: "theme",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "通知铃声",
|
title: "通知铃声",
|
||||||
|
|||||||
@ -100,26 +100,6 @@ const settingsDefinitions = {
|
|||||||
icon: "mdi-card-outline",
|
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": {
|
"timeCard.enabled": {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user