1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-02 17:29:23 +00:00
This commit is contained in:
SunWuyuan 2025-03-15 15:35:01 +08:00
parent e6cbe1faea
commit 49c93ecd08
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
7 changed files with 208 additions and 274 deletions

View File

@ -1,12 +1,7 @@
<template> <template>
<v-app> <v-app>
<v-main> <router-view />
<router-view v-slot="{ Component, route }"> <global-message />
<transition name="md3" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</v-main>
</v-app> </v-app>
</template> </template>

View File

@ -0,0 +1,65 @@
<template>
<v-snackbar
v-model="snackbar"
:color="colors[message?.type] || colors.info"
:timeout="4000"
location="bottom"
class="global-snackbar"
multi-line
>
<div class="d-flex align-center">
<v-icon :icon="icons[message?.type] || icons.info" class="mr-2" />
<div>
<div class="text-subtitle-2 font-weight-medium">{{ message?.title }}</div>
<div v-if="message?.content" class="text-body-2">{{ message?.content }}</div>
</div>
</div>
<template #actions>
<v-btn variant="text" icon="mdi-close" @click="snackbar = false" />
</template>
</v-snackbar>
</template>
<script>
import { defineComponent, ref, onBeforeUnmount } from 'vue';
import messageService from '@/utils/message';
export default defineComponent({
name: 'GlobalMessage',
setup() {
const snackbar = ref(false);
const message = ref(null);
const icons = {
success: 'mdi-check-circle',
error: 'mdi-alert-circle',
warning: 'mdi-alert',
info: 'mdi-information'
};
const colors = {
success: 'success',
error: 'error',
warning: 'warning',
info: 'info'
};
const unsubscribe = messageService?.onSnackbar?.((msg) => {
if (!msg) return;
message.value = msg;
snackbar.value = true;
});
onBeforeUnmount(() => unsubscribe?.());
return { snackbar, message, icons, colors };
}
});
</script>
<style scoped>
.global-snackbar {
max-width: 400px;
margin: 0 auto;
}
</style>

View File

@ -1,268 +1,111 @@
<template> <template>
<!-- 侧边栏 --> <v-navigation-drawer v-model="drawer" location="right" temporary width="400">
<v-navigation-drawer
v-model="drawer"
location="right"
temporary
width="400"
>
<v-toolbar color="primary"> <v-toolbar color="primary">
<v-toolbar-title class="text-white"> <v-toolbar-title>
消息记录 消息记录
<v-chip <v-chip v-if="unreadCount" color="error" size="x-small" class="ml-2">
v-if="unreadCount"
color="error"
size="small"
class="ml-2"
>
{{ unreadCount }} {{ unreadCount }}
</v-chip> </v-chip>
</v-toolbar-title> </v-toolbar-title>
<v-spacer /> <template #append>
<v-btn <v-btn icon="mdi-delete" variant="text" color="white" @click="clear" />
icon="mdi-delete" </template>
variant="text"
color="white"
@click="clearMessages"
/>
</v-toolbar> </v-toolbar>
<v-list class="message-list"> <v-list class="pa-4">
<template v-for="(msg, index) in visibleMessages" :key="msg.id"> <template v-if="visibleMessages.length">
<v-list-item <v-slide-y-transition group>
:class="{ 'unread': !msg.read }" <v-list-item
class="message-item mb-2" v-for="msg in visibleMessages"
@click="markAsRead(msg.id)" :key="msg.id"
> :active="!msg.read"
<template #prepend> class="mb-3"
<v-icon rounded
:icon="getIcon(msg.type)" @click="markAsRead(msg.id)"
:color="getColor(msg.type)" >
size="small" <template #prepend>
/> <v-icon :icon="icons[msg.type]" :color="colors[msg.type]" size="20" />
</template> </template>
<v-list-item-title>{{ msg.title }}</v-list-item-title> <div class="d-flex flex-column flex-grow-1 px-3">
<v-list-item-subtitle v-if="msg.content"> <v-list-item-title>{{ msg.title }}</v-list-item-title>
{{ msg.content }} <v-list-item-subtitle v-if="msg.content">{{ msg.content }}</v-list-item-subtitle>
</v-list-item-subtitle> <span class="text-caption text-grey">{{ new Date(msg.timestamp).toLocaleTimeString() }}</span>
<v-list-item-subtitle class="text-caption"> </div>
{{ formatTime(msg.timestamp) }}
</v-list-item-subtitle>
<template #append> <template #append>
<v-btn <v-btn icon="mdi-delete" variant="text" size="small" @click.stop="deleteMessage(msg.id)" />
icon="mdi-delete" </template>
variant="text" </v-list-item>
size="small" </v-slide-y-transition>
@click.stop="deleteMessage(msg.id)"
/> <v-btn v-if="hasMore" variant="tonal" block class="mt-4" @click="loadMore">
</template> 加载更多
</v-list-item> </v-btn>
<v-divider v-if="index < visibleMessages.length - 1" />
</template> </template>
<v-list-item v-else>
<v-btn <template #prepend>
v-if="hasMoreMessages" <v-icon icon="mdi-inbox" color="grey" />
block </template>
variant="text" <v-list-item-title class="text-grey">暂无消息</v-list-item-title>
@click="loadMoreMessages" </v-list-item>
>
加载更多
</v-btn>
</v-list> </v-list>
</v-navigation-drawer> </v-navigation-drawer>
<!-- 消息提示组 -->
<div class="message-container">
<TransitionGroup name="message">
<v-alert
v-for="msg in activeMessages"
:key="msg.id"
:type="msg.type"
variant="tonal"
closable
class="message-alert mb-2"
@click:close="removeActiveMessage(msg.id)"
>
<div class="d-flex align-center">
<span class="font-weight-medium">{{ msg.title }}</span>
<v-spacer />
<span class="text-caption">{{ formatTime(msg.timestamp) }}</span>
</div>
<div
v-if="msg.content"
class="message-content mt-1"
>
{{ msg.content }}
</div>
</v-alert>
</TransitionGroup>
</div>
</template> </template>
<script> <script>
import { defineComponent, ref, computed, onBeforeUnmount } from 'vue';
import messageService from '@/utils/message'; import messageService from '@/utils/message';
import { getSetting } from '@/utils/settings';
import { debounce } from '@/utils/debounce';
export default { export default defineComponent({
name: 'MessageLog', name: 'MessageLog',
data: () => ({ setup() {
drawer: false, const drawer = ref(false);
messages: [], const messages = ref([]);
activeMessages: [], const unreadCount = ref(0);
unreadCount: 0, const page = ref(1);
maxActiveMessages: getSetting('message.maxActiveMessages'), const PAGE_SIZE = 20;
messageTimeout: getSetting('message.timeout'),
showSidebar: getSetting('message.showSidebar'),
saveHistory: getSetting('message.saveHistory'),
pageSize: 20,
currentPage: 1,
}),
computed: { const visibleMessages = computed(() => messages.value.slice(0, page.value * PAGE_SIZE));
visibleMessages() { const hasMore = computed(() => visibleMessages.value.length < messages.value.length);
return this.messages.slice(0, this.currentPage * this.pageSize);
},
hasMoreMessages() {
return this.visibleMessages.length < this.messages.length;
}
},
created() { const icons = {
this.debouncedUpdateMessages = debounce(this.updateMessages, 300); success: 'mdi-check-circle',
}, error: 'mdi-alert-circle',
warning: 'mdi-alert',
info: 'mdi-information'
};
mounted() { const colors = {
messageService.initialize(); success: 'success',
messageService.onSnackbar(this.showMessage); error: 'error',
messageService.onLog(this.debouncedUpdateMessages); warning: 'warning',
}, info: 'primary'
};
methods: { const unsubscribe = messageService?.onLog?.(msgs => {
loadMoreMessages() { if (!msgs) return;
this.currentPage++; messages.value = msgs.reverse();
}, unreadCount.value = messageService.getUnreadCount();
});
showMessage(message) { onBeforeUnmount(() => unsubscribe?.());
if (!this.showSidebar) return;
this.activeMessages.unshift(message); return {
if (this.activeMessages.length > this.maxActiveMessages) { drawer,
this.activeMessages.pop(); unreadCount,
visibleMessages,
hasMore,
icons,
colors,
loadMore: () => page.value++,
markAsRead: id => messageService?.markAsRead?.(id),
deleteMessage: id => messageService?.deleteMessage?.(id),
clear: () => {
messageService?.clearMessages?.();
messages.value = [];
} }
};
setTimeout(() => {
this.removeActiveMessage(message.id);
}, this.messageTimeout);
},
removeActiveMessage(id) {
const index = this.activeMessages.findIndex(m => m.id === id);
if (index !== -1) {
this.activeMessages.splice(index, 1);
}
},
updateMessages(messages) {
if (this.saveHistory) {
this.messages = [...messages].reverse();
this.unreadCount = messageService.getUnreadCount();
}
},
markAsRead(id) {
messageService.markAsRead(id);
},
deleteMessage(id) {
messageService.deleteMessage(id);
},
clearMessages() {
messageService.clearMessages();
this.messages = [];
this.activeMessages = [];
},
getIcon(type) {
const icons = {
success: 'mdi-check-circle',
error: 'mdi-alert-circle',
warning: 'mdi-alert',
info: 'mdi-information'
};
return icons[type] || icons.info;
},
getColor(type) {
const colors = {
success: 'success',
error: 'error',
warning: 'warning',
info: 'primary'
};
return colors[type] || colors.info;
},
formatTime: (() => {
const cache = new Map();
return (timestamp) => {
if (cache.has(timestamp)) {
return cache.get(timestamp);
}
const formatted = new Date(timestamp).toLocaleTimeString();
cache.set(timestamp, formatted);
return formatted;
};
})()
} }
}; });
</script> </script>
<style scoped>
.message-container {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 1000;
max-width: 400px;
}
.message-alert {
width: 100%;
}
.message-list {
height: calc(100vh - 64px);
overflow-y: auto;
scroll-behavior: smooth;
will-change: transform;
}
.message-item {
border-left: 3px solid transparent;
contain: content;
}
.message-item.unread {
background-color: rgba(var(--v-theme-primary), 0.05);
}
/* 消息动画 */
.message-enter-active,
.message-leave-active {
transition: all 0.3s ease;
}
.message-enter-from {
opacity: 0;
transform: translateX(30px);
}
.message-leave-to {
opacity: 0;
transform: translateY(30px);
}
</style>

View File

@ -9,6 +9,7 @@ import { registerPlugins } from '@/plugins'
// Components // Components
import App from './App.vue' import App from './App.vue'
import GlobalMessage from '@/components/GlobalMessage.vue'
// Composables // Composables
import { createApp } from 'vue' import { createApp } from 'vue'
@ -21,4 +22,6 @@ registerPlugins(app)
app.use(messageService); app.use(messageService);
app.component('GlobalMessage', GlobalMessage)
app.mount('#app') app.mount('#app')

View File

@ -47,7 +47,7 @@
@click="$refs.messageLog.drawer = true" @click="$refs.messageLog.drawer = true"
/> />
</template> </template>
</v-app-bar>{{ state.boardData }} </v-app-bar>
<div class="d-flex"> <div class="d-flex">
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<v-container class="main-window flex-grow-1 no-select" fluid> <v-container class="main-window flex-grow-1 no-select" fluid>
@ -610,7 +610,6 @@ export default {
); );
if (!response.success) { if (!response.success) {
//
if (response.error.code === "NOT_FOUND") { if (response.error.code === "NOT_FOUND") {
this.state.showNoDataMessage = true; this.state.showNoDataMessage = true;
this.state.noDataMessage = response.error.message; this.state.noDataMessage = response.error.message;
@ -622,14 +621,13 @@ export default {
throw new Error(response.error.message); throw new Error(response.error.message);
} }
} else { } else {
//
this.state.boardData = response.data; this.state.boardData = response.data;
this.state.synced = true; this.state.synced = true;
this.state.showNoDataMessage = false; this.state.showNoDataMessage = false;
this.showMessage("下载成功", "数据已更新"); this.$message.success("下载成功", "数据已更新");
} }
} catch (error) { } catch (error) {
this.showError("下载失败", error.message); this.$message.error("下载失败", error.message);
} finally { } finally {
this.loading.download = false; this.loading.download = false;
} }
@ -658,7 +656,7 @@ export default {
await this.uploadData(); await this.uploadData();
return true; return true;
} catch (error) { } catch (error) {
this.showError('保存失败', error.message || '请重试'); this.$message.error('保存失败', error.message || '请重试');
return false; return false;
} }
}, },
@ -703,7 +701,7 @@ export default {
} }
this.state.synced = true; this.state.synced = true;
this.showMessage(response.message || "保存成功"); this.$message.success(response.message || "保存成功");
} finally { } finally {
this.loading.upload = false; this.loading.upload = false;
} }
@ -723,18 +721,12 @@ export default {
this.state.studentList = response.data.studentList || []; this.state.studentList = response.data.studentList || [];
} catch (error) { } catch (error) {
console.error("加载配置失败:", error); console.error("加载配置失败:", error);
this.showError("加载配置失败", error.message); this.$message.error("加载配置失败", error.message);
} }
}, },
showSyncMessage() { showSyncMessage() {
this.state.snackbar = true; this.$message.success("数据已同步", "数据已完成与服务器同步");
this.state.snackbarText = "数据已完成与服务器同步";
},
showError(message) {
this.state.snackbar = true;
this.state.snackbarText = message;
}, },
async openDialog(subject) { async openDialog(subject) {
@ -743,7 +735,7 @@ export default {
await this.downloadData(); await this.downloadData();
} catch (err) { } catch (err) {
console.error("刷新数据失败:", err); console.error("刷新数据失败:", err);
this.showError("刷新数据失败,可能显示的不是最新数据"); this.$message.error("刷新数据失败,可能显示的不是最新数据");
} }
} }
@ -1020,7 +1012,7 @@ export default {
this.state.attendanceDialog = false; this.state.attendanceDialog = false;
} catch (error) { } catch (error) {
console.error("保存出勤状态失败:", error); console.error("保存出勤状态失败:", error);
this.showError("保存失败,请重试"); this.$message.error("保存失败", "请重试");
} }
}, },

View File

@ -273,6 +273,25 @@
</template> </template>
</v-list-item> </v-list-item>
<v-divider class="my-2" />
<v-list-item>
<template #prepend>
<v-icon icon="mdi-database" class="mr-3" />
</template>
<v-list-item-title>禁用消息日志记录</v-list-item-title>
<v-list-item-subtitle>关闭保存消息到本地存储的功能</v-list-item-subtitle>
<template #append>
<v-switch
v-model="settings.developer.disableMessageLog"
density="comfortable"
hide-details
/>
</template>
</v-list-item>
<v-divider class="my-2" />
<v-expand-transition> <v-expand-transition>
<div v-if="settings.developer.showDebugConfig"> <div v-if="settings.developer.showDebugConfig">
<v-divider class="my-2" /> <v-divider class="my-2" />
@ -385,7 +404,8 @@ export default {
}, },
developer: { developer: {
enabled: getSetting('developer.enabled'), enabled: getSetting('developer.enabled'),
showDebugConfig: getSetting('developer.showDebugConfig') showDebugConfig: getSetting('developer.showDebugConfig'),
disableMessageLog: getSetting('developer.disableMessageLog')
}, },
message: { message: {
showSidebar: getSetting('message.showSidebar'), showSidebar: getSetting('message.showSidebar'),

View File

@ -110,6 +110,12 @@ const settingsDefinitions = {
default: false, default: false,
description: "是否显示调试配置", description: "是否显示调试配置",
}, },
// 新增的配置项禁止将消息日志记录到localStorage
"developer.disableMessageLog": {
type: "boolean",
default: false,
description: "是否禁用将消息日志记录到localStorage",
},
// 消息设置 // 消息设置
"message.showSidebar": { "message.showSidebar": {
@ -236,25 +242,35 @@ function getSetting(key) {
return null; return null;
} }
// 添加对开发者选项依赖的检查 // 确保开发者相关设置正确处理
if (definition.requireDeveloper && !settingsCache["developer.enabled"]) { if (definition.requireDeveloper) {
return definition.default; const devEnabled = settingsCache['developer.enabled'];
if (!devEnabled) {
return definition.default;
}
} }
const value = settingsCache[key]; const value = settingsCache[key];
return value !== undefined ? value : definition.default; return value !== undefined ? value : definition.default;
} }
// 添加设置变更日志函数 // 修改 logSettingsChange 函数,优化检查逻辑
function logSettingsChange(key, oldValue, newValue) { function logSettingsChange(key, oldValue, newValue) {
if ( // 确保设置已加载
settingsCache["developer.enabled"] && if (!settingsCache) {
settingsCache["developer.showDebugConfig"] loadSettings();
) { }
const shouldLog =
settingsCache['developer.enabled'] &&
settingsCache['developer.showDebugConfig'] &&
!settingsCache['developer.disableMessageLog'];
if (shouldLog) {
console.log(`[Settings] ${key}:`, { console.log(`[Settings] ${key}:`, {
old: oldValue, old: oldValue,
new: newValue, new: newValue,
time: new Date().toLocaleTimeString(), time: new Date().toLocaleTimeString()
}); });
} }
} }