mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-02 09:19:23 +00:00
1
This commit is contained in:
parent
49c93ecd08
commit
6b3ee0074e
@ -2,7 +2,7 @@
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:color="colors[message?.type] || colors.info"
|
||||
:timeout="4000"
|
||||
:timeout="2000"
|
||||
location="bottom"
|
||||
class="global-snackbar"
|
||||
multi-line
|
||||
@ -21,7 +21,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, onBeforeUnmount } from 'vue';
|
||||
import { defineComponent, ref, onBeforeUnmount, nextTick } from 'vue';
|
||||
import messageService from '@/utils/message';
|
||||
|
||||
export default defineComponent({
|
||||
@ -44,8 +44,12 @@ export default defineComponent({
|
||||
info: 'info'
|
||||
};
|
||||
|
||||
const unsubscribe = messageService?.onSnackbar?.((msg) => {
|
||||
const unsubscribe = messageService?.onSnackbar?.(async (msg) => {
|
||||
if (!msg) return;
|
||||
if (snackbar.value) {
|
||||
snackbar.value = false;
|
||||
await nextTick();
|
||||
}
|
||||
message.value = msg;
|
||||
snackbar.value = true;
|
||||
});
|
||||
|
@ -1,49 +1,37 @@
|
||||
<template>
|
||||
<v-navigation-drawer v-model="drawer" location="right" temporary width="400">
|
||||
<v-toolbar color="primary">
|
||||
<v-toolbar-title>
|
||||
消息记录
|
||||
<v-chip v-if="unreadCount" color="error" size="x-small" class="ml-2">
|
||||
{{ unreadCount }}
|
||||
</v-chip>
|
||||
</v-toolbar-title>
|
||||
<v-toolbar-title>消息记录</v-toolbar-title>
|
||||
<template #append>
|
||||
<v-btn icon="mdi-delete" variant="text" color="white" @click="clear" />
|
||||
<v-btn icon="mdi-delete" variant="text" @click="clearMessages" />
|
||||
</template>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list class="pa-4">
|
||||
<template v-if="visibleMessages.length">
|
||||
<v-slide-y-transition group>
|
||||
<v-list-item
|
||||
v-for="msg in visibleMessages"
|
||||
:key="msg.id"
|
||||
:active="!msg.read"
|
||||
class="mb-3"
|
||||
rounded
|
||||
@click="markAsRead(msg.id)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :icon="icons[msg.type]" :color="colors[msg.type]" size="20" />
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-for="msg in messages" :key="msg.id" rounded>
|
||||
<template #prepend>
|
||||
<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>
|
||||
<span class="text-caption text-grey">{{ new Date(msg.timestamp).toLocaleTimeString() }}</span>
|
||||
</div>
|
||||
<v-list-item-title>{{ msg.title }}</v-list-item-title>
|
||||
<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>
|
||||
|
||||
<template #append>
|
||||
<v-btn icon="mdi-delete" variant="text" size="small" @click.stop="deleteMessage(msg.id)" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-slide-y-transition>
|
||||
<template #append>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="deleteMessage(msg.id)"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-btn v-if="hasMore" variant="tonal" block class="mt-4" @click="loadMore">
|
||||
加载更多
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list-item v-else>
|
||||
<v-list-item v-if="!messages.length">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-inbox" color="grey" />
|
||||
</template>
|
||||
@ -54,58 +42,43 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref, computed, onBeforeUnmount } from 'vue';
|
||||
import messageService from '@/utils/message';
|
||||
import { defineComponent, ref } from "vue";
|
||||
import messageService from "@/utils/message";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MessageLog',
|
||||
name: "MessageLog",
|
||||
setup() {
|
||||
const drawer = ref(false);
|
||||
const messages = ref([]);
|
||||
const unreadCount = ref(0);
|
||||
const page = ref(1);
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
const visibleMessages = computed(() => messages.value.slice(0, page.value * PAGE_SIZE));
|
||||
const hasMore = computed(() => visibleMessages.value.length < messages.value.length);
|
||||
|
||||
const icons = {
|
||||
success: 'mdi-check-circle',
|
||||
error: 'mdi-alert-circle',
|
||||
warning: 'mdi-alert',
|
||||
info: 'mdi-information'
|
||||
success: "mdi-check-circle",
|
||||
error: "mdi-alert-circle",
|
||||
warning: "mdi-alert",
|
||||
info: "mdi-information",
|
||||
};
|
||||
|
||||
const colors = {
|
||||
success: 'success',
|
||||
error: 'error',
|
||||
warning: 'warning',
|
||||
info: 'primary'
|
||||
success: "success",
|
||||
error: "error",
|
||||
warning: "warning",
|
||||
info: "primary",
|
||||
};
|
||||
|
||||
const unsubscribe = messageService?.onLog?.(msgs => {
|
||||
if (!msgs) return;
|
||||
messages.value = msgs.reverse();
|
||||
unreadCount.value = messageService.getUnreadCount();
|
||||
messageService.onLog((msgs) => {
|
||||
if (msgs) {
|
||||
messages.value = msgs;
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => unsubscribe?.());
|
||||
|
||||
return {
|
||||
drawer,
|
||||
unreadCount,
|
||||
visibleMessages,
|
||||
hasMore,
|
||||
messages,
|
||||
icons,
|
||||
colors,
|
||||
loadMore: () => page.value++,
|
||||
markAsRead: id => messageService?.markAsRead?.(id),
|
||||
deleteMessage: id => messageService?.deleteMessage?.(id),
|
||||
clear: () => {
|
||||
messageService?.clearMessages?.();
|
||||
messages.value = [];
|
||||
}
|
||||
deleteMessage: (id) => messageService.deleteMessage(id),
|
||||
clearMessages: () => messageService.clearMessages(),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -275,23 +275,6 @@
|
||||
|
||||
<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" />
|
||||
@ -404,8 +387,7 @@ export default {
|
||||
},
|
||||
developer: {
|
||||
enabled: getSetting('developer.enabled'),
|
||||
showDebugConfig: getSetting('developer.showDebugConfig'),
|
||||
disableMessageLog: getSetting('developer.disableMessageLog')
|
||||
showDebugConfig: getSetting('developer.showDebugConfig')
|
||||
},
|
||||
message: {
|
||||
showSidebar: getSetting('message.showSidebar'),
|
||||
|
104
src/utils/db.js
Normal file
104
src/utils/db.js
Normal file
@ -0,0 +1,104 @@
|
||||
const DB_NAME = 'homeworkboard';
|
||||
const DB_VERSION = 1;
|
||||
const LOG_STORE = 'message_logs';
|
||||
|
||||
class LogDB {
|
||||
constructor() {
|
||||
this.db = null;
|
||||
this.ready = this.initDB();
|
||||
}
|
||||
|
||||
async initDB() {
|
||||
try {
|
||||
this.db = await new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(LOG_STORE)) {
|
||||
const store = db.createObjectStore(LOG_STORE, {
|
||||
keyPath: 'id'
|
||||
});
|
||||
// 只保留时间和类型索引
|
||||
store.createIndex('timestamp', 'timestamp');
|
||||
store.createIndex('type', 'type');
|
||||
}
|
||||
};
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('初始化日志数据库失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async ensureDB() {
|
||||
if (!this.db) {
|
||||
await this.ready;
|
||||
}
|
||||
if (!this.db) {
|
||||
throw new Error('数据库未初始化');
|
||||
}
|
||||
}
|
||||
|
||||
async addLog(message) {
|
||||
await this.ensureDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([LOG_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(LOG_STORE);
|
||||
|
||||
const request = store.add(message);
|
||||
|
||||
transaction.oncomplete = () => resolve(request.result);
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getLogs(limit = 20) {
|
||||
await this.ensureDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([LOG_STORE], 'readonly');
|
||||
const store = transaction.objectStore(LOG_STORE);
|
||||
const index = store.index('timestamp');
|
||||
|
||||
const request = index.getAll(null, limit);
|
||||
|
||||
request.onsuccess = () => resolve(request.result.reverse());
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteLog(messageId) {
|
||||
await this.ensureDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([LOG_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(LOG_STORE);
|
||||
|
||||
const request = store.delete(messageId);
|
||||
|
||||
transaction.oncomplete = () => resolve(true);
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
|
||||
async clearLogs() {
|
||||
await this.ensureDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([LOG_STORE], 'readwrite');
|
||||
const store = transaction.objectStore(LOG_STORE);
|
||||
|
||||
const request = store.clear();
|
||||
|
||||
transaction.oncomplete = () => resolve(true);
|
||||
transaction.onerror = () => reject(transaction.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new LogDB();
|
@ -1,3 +1,6 @@
|
||||
import logDB from './db';
|
||||
import { getSetting } from './settings';
|
||||
|
||||
const messages = [];
|
||||
let snackbarCallback = null;
|
||||
let logCallback = null;
|
||||
@ -15,79 +18,27 @@ const defaultOptions = {
|
||||
addToLog: true
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'classworks_messages';
|
||||
const MAX_MESSAGES = 100; // 最大消息数量
|
||||
const MAX_STORAGE_SIZE = 1024 * 1024; // 1MB 存储限制
|
||||
|
||||
// 加载保存的消息
|
||||
function loadStoredMessages() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
messages.push(...JSON.parse(stored));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理旧消息
|
||||
function cleanOldMessages() {
|
||||
if (messages.length > MAX_MESSAGES) {
|
||||
messages.splice(MAX_MESSAGES);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查存储大小
|
||||
function checkStorageSize(data) {
|
||||
try {
|
||||
const size = new Blob([data]).size;
|
||||
return size <= MAX_STORAGE_SIZE;
|
||||
} catch (error) {
|
||||
console.error('检查存储大小失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存消息到localStorage
|
||||
function saveMessages() {
|
||||
try {
|
||||
cleanOldMessages();
|
||||
const data = JSON.stringify(messages);
|
||||
|
||||
if (!checkStorageSize(data)) {
|
||||
// 如果数据太大,删除一半的旧消息
|
||||
messages.splice(Math.floor(messages.length / 2));
|
||||
return saveMessages();
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, data);
|
||||
} catch (error) {
|
||||
if (error.name === 'QuotaExceededError') {
|
||||
// 如果存储空间不足,清理一些旧消息再试
|
||||
messages.splice(Math.floor(messages.length / 2));
|
||||
return saveMessages();
|
||||
}
|
||||
console.error('保存消息历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function createMessage(type, title, content = '', options = {}) {
|
||||
async function createMessage(type, title, content = '', options = {}) {
|
||||
const msgOptions = { ...defaultOptions, ...options };
|
||||
const message = {
|
||||
id: Date.now() + Math.random(),
|
||||
type,
|
||||
title,
|
||||
content: content.substring(0, 500), // 限制内容长度
|
||||
timestamp: new Date(),
|
||||
read: false
|
||||
content: content.substring(0, 500),
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
if (msgOptions.addToLog) {
|
||||
messages.unshift(message); // 新消息添加到开头
|
||||
cleanOldMessages();
|
||||
saveMessages();
|
||||
logCallback?.(messages);
|
||||
try {
|
||||
await logDB.addLog(message);
|
||||
messages.unshift(message);
|
||||
while (messages.length > getSetting('message.maxActiveMessages')) {
|
||||
messages.pop();
|
||||
}
|
||||
logCallback?.(messages);
|
||||
} catch (error) {
|
||||
console.error('保存日志失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (msgOptions.showSnackbar) {
|
||||
@ -97,7 +48,6 @@ function createMessage(type, title, content = '', options = {}) {
|
||||
return message;
|
||||
}
|
||||
|
||||
// 添加防抖函数实现
|
||||
function debounce(fn, delay) {
|
||||
let timer = null;
|
||||
return function (...args) {
|
||||
@ -119,35 +69,37 @@ export default {
|
||||
},
|
||||
onSnackbar: (callback) => { snackbarCallback = callback; },
|
||||
onLog: (callback) => { logCallback = callback; },
|
||||
getMessages: () => [...messages],
|
||||
clearMessages: () => {
|
||||
messages.length = 0;
|
||||
getMessages: async () => {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
return await logDB.getLogs();
|
||||
} catch (error) {
|
||||
console.error('清除消息历史失败:', error);
|
||||
console.error('获取日志失败:', error);
|
||||
return [...messages];
|
||||
}
|
||||
},
|
||||
clearMessages: async () => {
|
||||
try {
|
||||
await logDB.clearLogs();
|
||||
messages.length = 0;
|
||||
logCallback?.(messages);
|
||||
} catch (error) {
|
||||
console.error('清除日志失败:', error);
|
||||
}
|
||||
},
|
||||
MessageType,
|
||||
markAsRead: (messageId) => {
|
||||
const message = messages.find(m => m.id === messageId);
|
||||
if (message) {
|
||||
message.read = true;
|
||||
saveMessages();
|
||||
markAsRead: () => {}, // 移除标记已读功能
|
||||
deleteMessage: async (messageId) => {
|
||||
try {
|
||||
await logDB.deleteLog(messageId);
|
||||
const index = messages.findIndex(m => m.id === messageId);
|
||||
if (index !== -1) {
|
||||
messages.splice(index, 1);
|
||||
}
|
||||
logCallback?.(messages);
|
||||
} catch (error) {
|
||||
console.error('删除消息失败:', error);
|
||||
}
|
||||
},
|
||||
deleteMessage: (messageId) => {
|
||||
const index = messages.findIndex(m => m.id === messageId);
|
||||
if (index !== -1) {
|
||||
messages.splice(index, 1);
|
||||
saveMessages();
|
||||
logCallback?.(messages);
|
||||
}
|
||||
},
|
||||
getUnreadCount: () => messages.filter(m => !m.read).length,
|
||||
initialize: () => {
|
||||
loadStoredMessages();
|
||||
},
|
||||
debounce, // 导出防抖函数
|
||||
getUnreadCount: () => 0, // 移除未读计数
|
||||
debounce,
|
||||
};
|
||||
|
@ -110,11 +110,11 @@ const settingsDefinitions = {
|
||||
default: false,
|
||||
description: "是否显示调试配置",
|
||||
},
|
||||
// 新增的配置项:禁止将消息日志记录到localStorage
|
||||
"developer.disableMessageLog": {
|
||||
"developer.disableMessageLog": { // 添加新的设置项
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "是否禁用将消息日志记录到localStorage",
|
||||
description: "禁用消息日志记录",
|
||||
requireDeveloper: true,
|
||||
},
|
||||
|
||||
// 消息设置
|
||||
@ -263,8 +263,7 @@ function logSettingsChange(key, oldValue, newValue) {
|
||||
|
||||
const shouldLog =
|
||||
settingsCache['developer.enabled'] &&
|
||||
settingsCache['developer.showDebugConfig'] &&
|
||||
!settingsCache['developer.disableMessageLog'];
|
||||
settingsCache['developer.showDebugConfig'];
|
||||
|
||||
if (shouldLog) {
|
||||
console.log(`[Settings] ${key}:`, {
|
||||
|
Loading…
x
Reference in New Issue
Block a user