mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-02 17:29:23 +00:00
1
This commit is contained in:
parent
e6cbe1faea
commit
49c93ecd08
@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="md3" mode="out-in">
|
||||
<component :is="Component" :key="route.path" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</v-main>
|
||||
<router-view />
|
||||
<global-message />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
|
65
src/components/GlobalMessage.vue
Normal file
65
src/components/GlobalMessage.vue
Normal 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>
|
@ -1,268 +1,111 @@
|
||||
<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-title class="text-white">
|
||||
<v-toolbar-title>
|
||||
消息记录
|
||||
<v-chip
|
||||
v-if="unreadCount"
|
||||
color="error"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
<v-chip v-if="unreadCount" color="error" size="x-small" class="ml-2">
|
||||
{{ unreadCount }}
|
||||
</v-chip>
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
color="white"
|
||||
@click="clearMessages"
|
||||
/>
|
||||
<template #append>
|
||||
<v-btn icon="mdi-delete" variant="text" color="white" @click="clear" />
|
||||
</template>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list class="message-list">
|
||||
<template v-for="(msg, index) in visibleMessages" :key="msg.id">
|
||||
<v-list class="pa-4">
|
||||
<template v-if="visibleMessages.length">
|
||||
<v-slide-y-transition group>
|
||||
<v-list-item
|
||||
:class="{ 'unread': !msg.read }"
|
||||
class="message-item mb-2"
|
||||
v-for="msg in visibleMessages"
|
||||
:key="msg.id"
|
||||
:active="!msg.read"
|
||||
class="mb-3"
|
||||
rounded
|
||||
@click="markAsRead(msg.id)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
:icon="getIcon(msg.type)"
|
||||
:color="getColor(msg.type)"
|
||||
size="small"
|
||||
/>
|
||||
<v-icon :icon="icons[msg.type]" :color="colors[msg.type]" size="20" />
|
||||
</template>
|
||||
|
||||
<div class="d-flex flex-column flex-grow-1 px-3">
|
||||
<v-list-item-title>{{ msg.title }}</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="msg.content">
|
||||
{{ msg.content }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
{{ formatTime(msg.timestamp) }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-subtitle v-if="msg.content">{{ msg.content }}</v-list-item-subtitle>
|
||||
<span class="text-caption text-grey">{{ new Date(msg.timestamp).toLocaleTimeString() }}</span>
|
||||
</div>
|
||||
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click.stop="deleteMessage(msg.id)"
|
||||
/>
|
||||
<v-btn icon="mdi-delete" variant="text" size="small" @click.stop="deleteMessage(msg.id)" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider v-if="index < visibleMessages.length - 1" />
|
||||
</template>
|
||||
</v-slide-y-transition>
|
||||
|
||||
<v-btn
|
||||
v-if="hasMoreMessages"
|
||||
block
|
||||
variant="text"
|
||||
@click="loadMoreMessages"
|
||||
>
|
||||
<v-btn v-if="hasMore" variant="tonal" block class="mt-4" @click="loadMore">
|
||||
加载更多
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list-item v-else>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-inbox" color="grey" />
|
||||
</template>
|
||||
<v-list-item-title class="text-grey">暂无消息</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</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>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, computed, onBeforeUnmount } from 'vue';
|
||||
import messageService from '@/utils/message';
|
||||
import { getSetting } from '@/utils/settings';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'MessageLog',
|
||||
data: () => ({
|
||||
drawer: false,
|
||||
messages: [],
|
||||
activeMessages: [],
|
||||
unreadCount: 0,
|
||||
maxActiveMessages: getSetting('message.maxActiveMessages'),
|
||||
messageTimeout: getSetting('message.timeout'),
|
||||
showSidebar: getSetting('message.showSidebar'),
|
||||
saveHistory: getSetting('message.saveHistory'),
|
||||
pageSize: 20,
|
||||
currentPage: 1,
|
||||
}),
|
||||
setup() {
|
||||
const drawer = ref(false);
|
||||
const messages = ref([]);
|
||||
const unreadCount = ref(0);
|
||||
const page = ref(1);
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
computed: {
|
||||
visibleMessages() {
|
||||
return this.messages.slice(0, this.currentPage * this.pageSize);
|
||||
},
|
||||
hasMoreMessages() {
|
||||
return this.visibleMessages.length < this.messages.length;
|
||||
}
|
||||
},
|
||||
const visibleMessages = computed(() => messages.value.slice(0, page.value * PAGE_SIZE));
|
||||
const hasMore = computed(() => visibleMessages.value.length < messages.value.length);
|
||||
|
||||
created() {
|
||||
this.debouncedUpdateMessages = debounce(this.updateMessages, 300);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
messageService.initialize();
|
||||
messageService.onSnackbar(this.showMessage);
|
||||
messageService.onLog(this.debouncedUpdateMessages);
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadMoreMessages() {
|
||||
this.currentPage++;
|
||||
},
|
||||
|
||||
showMessage(message) {
|
||||
if (!this.showSidebar) return;
|
||||
|
||||
this.activeMessages.unshift(message);
|
||||
if (this.activeMessages.length > this.maxActiveMessages) {
|
||||
this.activeMessages.pop();
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
})()
|
||||
const unsubscribe = messageService?.onLog?.(msgs => {
|
||||
if (!msgs) return;
|
||||
messages.value = msgs.reverse();
|
||||
unreadCount.value = messageService.getUnreadCount();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => unsubscribe?.());
|
||||
|
||||
return {
|
||||
drawer,
|
||||
unreadCount,
|
||||
visibleMessages,
|
||||
hasMore,
|
||||
icons,
|
||||
colors,
|
||||
loadMore: () => page.value++,
|
||||
markAsRead: id => messageService?.markAsRead?.(id),
|
||||
deleteMessage: id => messageService?.deleteMessage?.(id),
|
||||
clear: () => {
|
||||
messageService?.clearMessages?.();
|
||||
messages.value = [];
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
</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>
|
||||
|
@ -9,6 +9,7 @@ import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
import GlobalMessage from '@/components/GlobalMessage.vue'
|
||||
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
@ -21,4 +22,6 @@ registerPlugins(app)
|
||||
|
||||
app.use(messageService);
|
||||
|
||||
app.component('GlobalMessage', GlobalMessage)
|
||||
|
||||
app.mount('#app')
|
||||
|
@ -47,7 +47,7 @@
|
||||
@click="$refs.messageLog.drawer = true"
|
||||
/>
|
||||
</template>
|
||||
</v-app-bar>{{ state.boardData }}
|
||||
</v-app-bar>
|
||||
<div class="d-flex">
|
||||
<!-- 主要内容区域 -->
|
||||
<v-container class="main-window flex-grow-1 no-select" fluid>
|
||||
@ -610,7 +610,6 @@ export default {
|
||||
);
|
||||
|
||||
if (!response.success) {
|
||||
// 处理错误情况
|
||||
if (response.error.code === "NOT_FOUND") {
|
||||
this.state.showNoDataMessage = true;
|
||||
this.state.noDataMessage = response.error.message;
|
||||
@ -622,14 +621,13 @@ export default {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
} else {
|
||||
// 处理成功情况
|
||||
this.state.boardData = response.data;
|
||||
this.state.synced = true;
|
||||
this.state.showNoDataMessage = false;
|
||||
this.showMessage("下载成功", "数据已更新");
|
||||
this.$message.success("下载成功", "数据已更新");
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError("下载失败", error.message);
|
||||
this.$message.error("下载失败", error.message);
|
||||
} finally {
|
||||
this.loading.download = false;
|
||||
}
|
||||
@ -658,7 +656,7 @@ export default {
|
||||
await this.uploadData();
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.showError('保存失败', error.message || '请重试');
|
||||
this.$message.error('保存失败', error.message || '请重试');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
@ -703,7 +701,7 @@ export default {
|
||||
}
|
||||
|
||||
this.state.synced = true;
|
||||
this.showMessage(response.message || "保存成功");
|
||||
this.$message.success(response.message || "保存成功");
|
||||
} finally {
|
||||
this.loading.upload = false;
|
||||
}
|
||||
@ -723,18 +721,12 @@ export default {
|
||||
this.state.studentList = response.data.studentList || [];
|
||||
} catch (error) {
|
||||
console.error("加载配置失败:", error);
|
||||
this.showError("加载配置失败", error.message);
|
||||
this.$message.error("加载配置失败", error.message);
|
||||
}
|
||||
},
|
||||
|
||||
showSyncMessage() {
|
||||
this.state.snackbar = true;
|
||||
this.state.snackbarText = "数据已完成与服务器同步";
|
||||
},
|
||||
|
||||
showError(message) {
|
||||
this.state.snackbar = true;
|
||||
this.state.snackbarText = message;
|
||||
this.$message.success("数据已同步", "数据已完成与服务器同步");
|
||||
},
|
||||
|
||||
async openDialog(subject) {
|
||||
@ -743,7 +735,7 @@ export default {
|
||||
await this.downloadData();
|
||||
} catch (err) {
|
||||
console.error("刷新数据失败:", err);
|
||||
this.showError("刷新数据失败,可能显示的不是最新数据");
|
||||
this.$message.error("刷新数据失败,可能显示的不是最新数据");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1020,7 +1012,7 @@ export default {
|
||||
this.state.attendanceDialog = false;
|
||||
} catch (error) {
|
||||
console.error("保存出勤状态失败:", error);
|
||||
this.showError("保存失败,请重试");
|
||||
this.$message.error("保存失败", "请重试");
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -273,6 +273,25 @@
|
||||
</template>
|
||||
</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>
|
||||
<div v-if="settings.developer.showDebugConfig">
|
||||
<v-divider class="my-2" />
|
||||
@ -385,7 +404,8 @@ export default {
|
||||
},
|
||||
developer: {
|
||||
enabled: getSetting('developer.enabled'),
|
||||
showDebugConfig: getSetting('developer.showDebugConfig')
|
||||
showDebugConfig: getSetting('developer.showDebugConfig'),
|
||||
disableMessageLog: getSetting('developer.disableMessageLog')
|
||||
},
|
||||
message: {
|
||||
showSidebar: getSetting('message.showSidebar'),
|
||||
|
@ -110,6 +110,12 @@ const settingsDefinitions = {
|
||||
default: false,
|
||||
description: "是否显示调试配置",
|
||||
},
|
||||
// 新增的配置项:禁止将消息日志记录到localStorage
|
||||
"developer.disableMessageLog": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "是否禁用将消息日志记录到localStorage",
|
||||
},
|
||||
|
||||
// 消息设置
|
||||
"message.showSidebar": {
|
||||
@ -236,25 +242,35 @@ function getSetting(key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 添加对开发者选项依赖的检查
|
||||
if (definition.requireDeveloper && !settingsCache["developer.enabled"]) {
|
||||
// 确保开发者相关设置正确处理
|
||||
if (definition.requireDeveloper) {
|
||||
const devEnabled = settingsCache['developer.enabled'];
|
||||
if (!devEnabled) {
|
||||
return definition.default;
|
||||
}
|
||||
}
|
||||
|
||||
const value = settingsCache[key];
|
||||
return value !== undefined ? value : definition.default;
|
||||
}
|
||||
|
||||
// 添加设置变更日志函数
|
||||
// 修改 logSettingsChange 函数,优化检查逻辑
|
||||
function logSettingsChange(key, oldValue, newValue) {
|
||||
if (
|
||||
settingsCache["developer.enabled"] &&
|
||||
settingsCache["developer.showDebugConfig"]
|
||||
) {
|
||||
// 确保设置已加载
|
||||
if (!settingsCache) {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
const shouldLog =
|
||||
settingsCache['developer.enabled'] &&
|
||||
settingsCache['developer.showDebugConfig'] &&
|
||||
!settingsCache['developer.disableMessageLog'];
|
||||
|
||||
if (shouldLog) {
|
||||
console.log(`[Settings] ${key}:`, {
|
||||
old: oldValue,
|
||||
new: newValue,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
time: new Date().toLocaleTimeString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user