mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-05 11:09:29 +00:00
1
This commit is contained in:
parent
49c93ecd08
commit
6b3ee0074e
@ -2,7 +2,7 @@
|
|||||||
<v-snackbar
|
<v-snackbar
|
||||||
v-model="snackbar"
|
v-model="snackbar"
|
||||||
:color="colors[message?.type] || colors.info"
|
:color="colors[message?.type] || colors.info"
|
||||||
:timeout="4000"
|
:timeout="2000"
|
||||||
location="bottom"
|
location="bottom"
|
||||||
class="global-snackbar"
|
class="global-snackbar"
|
||||||
multi-line
|
multi-line
|
||||||
@ -21,7 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { defineComponent, ref, onBeforeUnmount } from 'vue';
|
import { defineComponent, ref, onBeforeUnmount, nextTick } from 'vue';
|
||||||
import messageService from '@/utils/message';
|
import messageService from '@/utils/message';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -44,8 +44,12 @@ export default defineComponent({
|
|||||||
info: 'info'
|
info: 'info'
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsubscribe = messageService?.onSnackbar?.((msg) => {
|
const unsubscribe = messageService?.onSnackbar?.(async (msg) => {
|
||||||
if (!msg) return;
|
if (!msg) return;
|
||||||
|
if (snackbar.value) {
|
||||||
|
snackbar.value = false;
|
||||||
|
await nextTick();
|
||||||
|
}
|
||||||
message.value = msg;
|
message.value = msg;
|
||||||
snackbar.value = true;
|
snackbar.value = true;
|
||||||
});
|
});
|
||||||
|
@ -1,49 +1,37 @@
|
|||||||
<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>
|
<v-toolbar-title>消息记录</v-toolbar-title>
|
||||||
消息记录
|
|
||||||
<v-chip v-if="unreadCount" color="error" size="x-small" class="ml-2">
|
|
||||||
{{ unreadCount }}
|
|
||||||
</v-chip>
|
|
||||||
</v-toolbar-title>
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<v-btn icon="mdi-delete" variant="text" color="white" @click="clear" />
|
<v-btn icon="mdi-delete" variant="text" @click="clearMessages" />
|
||||||
</template>
|
</template>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
<v-list class="pa-4">
|
<v-list>
|
||||||
<template v-if="visibleMessages.length">
|
<v-list-item v-for="msg in messages" :key="msg.id" rounded>
|
||||||
<v-slide-y-transition group>
|
<template #prepend>
|
||||||
<v-list-item
|
<v-icon :icon="icons[msg.type]" :color="colors[msg.type]" size="20" />
|
||||||
v-for="msg in visibleMessages"
|
</template>
|
||||||
: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>
|
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-grow-1 px-3">
|
<v-list-item-title>{{ msg.title }}</v-list-item-title>
|
||||||
<v-list-item-title>{{ msg.title }}</v-list-item-title>
|
<v-list-item-subtitle v-if="msg.content">{{
|
||||||
<v-list-item-subtitle v-if="msg.content">{{ msg.content }}</v-list-item-subtitle>
|
msg.content
|
||||||
<span class="text-caption text-grey">{{ new Date(msg.timestamp).toLocaleTimeString() }}</span>
|
}}</v-list-item-subtitle>
|
||||||
</div>
|
<span class="text-caption text-grey">
|
||||||
|
{{ new Date(msg.timestamp).toLocaleTimeString() }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<template #append>
|
<template #append>
|
||||||
<v-btn icon="mdi-delete" variant="text" size="small" @click.stop="deleteMessage(msg.id)" />
|
<v-btn
|
||||||
</template>
|
icon="mdi-delete"
|
||||||
</v-list-item>
|
variant="text"
|
||||||
</v-slide-y-transition>
|
size="small"
|
||||||
|
@click="deleteMessage(msg.id)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
<v-btn v-if="hasMore" variant="tonal" block class="mt-4" @click="loadMore">
|
<v-list-item v-if="!messages.length">
|
||||||
加载更多
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list-item v-else>
|
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon icon="mdi-inbox" color="grey" />
|
<v-icon icon="mdi-inbox" color="grey" />
|
||||||
</template>
|
</template>
|
||||||
@ -54,58 +42,43 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { defineComponent, ref, computed, onBeforeUnmount } from 'vue';
|
import { defineComponent, ref } from "vue";
|
||||||
import messageService from '@/utils/message';
|
import messageService from "@/utils/message";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'MessageLog',
|
name: "MessageLog",
|
||||||
setup() {
|
setup() {
|
||||||
const drawer = ref(false);
|
const drawer = ref(false);
|
||||||
const messages = ref([]);
|
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 = {
|
const icons = {
|
||||||
success: 'mdi-check-circle',
|
success: "mdi-check-circle",
|
||||||
error: 'mdi-alert-circle',
|
error: "mdi-alert-circle",
|
||||||
warning: 'mdi-alert',
|
warning: "mdi-alert",
|
||||||
info: 'mdi-information'
|
info: "mdi-information",
|
||||||
};
|
};
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
success: 'success',
|
success: "success",
|
||||||
error: 'error',
|
error: "error",
|
||||||
warning: 'warning',
|
warning: "warning",
|
||||||
info: 'primary'
|
info: "primary",
|
||||||
};
|
};
|
||||||
|
|
||||||
const unsubscribe = messageService?.onLog?.(msgs => {
|
messageService.onLog((msgs) => {
|
||||||
if (!msgs) return;
|
if (msgs) {
|
||||||
messages.value = msgs.reverse();
|
messages.value = msgs;
|
||||||
unreadCount.value = messageService.getUnreadCount();
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => unsubscribe?.());
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
drawer,
|
drawer,
|
||||||
unreadCount,
|
messages,
|
||||||
visibleMessages,
|
|
||||||
hasMore,
|
|
||||||
icons,
|
icons,
|
||||||
colors,
|
colors,
|
||||||
loadMore: () => page.value++,
|
deleteMessage: (id) => messageService.deleteMessage(id),
|
||||||
markAsRead: id => messageService?.markAsRead?.(id),
|
clearMessages: () => messageService.clearMessages(),
|
||||||
deleteMessage: id => messageService?.deleteMessage?.(id),
|
|
||||||
clear: () => {
|
|
||||||
messageService?.clearMessages?.();
|
|
||||||
messages.value = [];
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -275,23 +275,6 @@
|
|||||||
|
|
||||||
<v-divider class="my-2" />
|
<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" />
|
||||||
@ -404,8 +387,7 @@ 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'),
|
||||||
|
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 = [];
|
const messages = [];
|
||||||
let snackbarCallback = null;
|
let snackbarCallback = null;
|
||||||
let logCallback = null;
|
let logCallback = null;
|
||||||
@ -15,79 +18,27 @@ const defaultOptions = {
|
|||||||
addToLog: true
|
addToLog: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = 'classworks_messages';
|
async function createMessage(type, title, content = '', options = {}) {
|
||||||
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 = {}) {
|
|
||||||
const msgOptions = { ...defaultOptions, ...options };
|
const msgOptions = { ...defaultOptions, ...options };
|
||||||
const message = {
|
const message = {
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
content: content.substring(0, 500), // 限制内容长度
|
content: content.substring(0, 500),
|
||||||
timestamp: new Date(),
|
timestamp: new Date()
|
||||||
read: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (msgOptions.addToLog) {
|
if (msgOptions.addToLog) {
|
||||||
messages.unshift(message); // 新消息添加到开头
|
try {
|
||||||
cleanOldMessages();
|
await logDB.addLog(message);
|
||||||
saveMessages();
|
messages.unshift(message);
|
||||||
logCallback?.(messages);
|
while (messages.length > getSetting('message.maxActiveMessages')) {
|
||||||
|
messages.pop();
|
||||||
|
}
|
||||||
|
logCallback?.(messages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存日志失败:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msgOptions.showSnackbar) {
|
if (msgOptions.showSnackbar) {
|
||||||
@ -97,7 +48,6 @@ function createMessage(type, title, content = '', options = {}) {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加防抖函数实现
|
|
||||||
function debounce(fn, delay) {
|
function debounce(fn, delay) {
|
||||||
let timer = null;
|
let timer = null;
|
||||||
return function (...args) {
|
return function (...args) {
|
||||||
@ -119,35 +69,37 @@ export default {
|
|||||||
},
|
},
|
||||||
onSnackbar: (callback) => { snackbarCallback = callback; },
|
onSnackbar: (callback) => { snackbarCallback = callback; },
|
||||||
onLog: (callback) => { logCallback = callback; },
|
onLog: (callback) => { logCallback = callback; },
|
||||||
getMessages: () => [...messages],
|
getMessages: async () => {
|
||||||
clearMessages: () => {
|
|
||||||
messages.length = 0;
|
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
return await logDB.getLogs();
|
||||||
} catch (error) {
|
} 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,
|
MessageType,
|
||||||
markAsRead: (messageId) => {
|
markAsRead: () => {}, // 移除标记已读功能
|
||||||
const message = messages.find(m => m.id === messageId);
|
deleteMessage: async (messageId) => {
|
||||||
if (message) {
|
try {
|
||||||
message.read = true;
|
await logDB.deleteLog(messageId);
|
||||||
saveMessages();
|
const index = messages.findIndex(m => m.id === messageId);
|
||||||
|
if (index !== -1) {
|
||||||
|
messages.splice(index, 1);
|
||||||
|
}
|
||||||
logCallback?.(messages);
|
logCallback?.(messages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除消息失败:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteMessage: (messageId) => {
|
getUnreadCount: () => 0, // 移除未读计数
|
||||||
const index = messages.findIndex(m => m.id === messageId);
|
debounce,
|
||||||
if (index !== -1) {
|
|
||||||
messages.splice(index, 1);
|
|
||||||
saveMessages();
|
|
||||||
logCallback?.(messages);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getUnreadCount: () => messages.filter(m => !m.read).length,
|
|
||||||
initialize: () => {
|
|
||||||
loadStoredMessages();
|
|
||||||
},
|
|
||||||
debounce, // 导出防抖函数
|
|
||||||
};
|
};
|
||||||
|
@ -110,11 +110,11 @@ const settingsDefinitions = {
|
|||||||
default: false,
|
default: false,
|
||||||
description: "是否显示调试配置",
|
description: "是否显示调试配置",
|
||||||
},
|
},
|
||||||
// 新增的配置项:禁止将消息日志记录到localStorage
|
"developer.disableMessageLog": { // 添加新的设置项
|
||||||
"developer.disableMessageLog": {
|
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
default: false,
|
default: false,
|
||||||
description: "是否禁用将消息日志记录到localStorage",
|
description: "禁用消息日志记录",
|
||||||
|
requireDeveloper: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 消息设置
|
// 消息设置
|
||||||
@ -263,8 +263,7 @@ function logSettingsChange(key, oldValue, newValue) {
|
|||||||
|
|
||||||
const shouldLog =
|
const shouldLog =
|
||||||
settingsCache['developer.enabled'] &&
|
settingsCache['developer.enabled'] &&
|
||||||
settingsCache['developer.showDebugConfig'] &&
|
settingsCache['developer.showDebugConfig'];
|
||||||
!settingsCache['developer.disableMessageLog'];
|
|
||||||
|
|
||||||
if (shouldLog) {
|
if (shouldLog) {
|
||||||
console.log(`[Settings] ${key}:`, {
|
console.log(`[Settings] ${key}:`, {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user