From 96c4ab81d99719c1bdc57a0c0a4d05322c4c3804 Mon Sep 17 00:00:00 2001 From: SunWuyuan Date: Sun, 9 Mar 2025 14:59:35 +0800 Subject: [PATCH] 1 --- package.json | 1 + pnpm-lock.yaml | 8 + src/components/settings/StudentListCard.vue | 161 ++++++++++++-------- src/pages/index.vue | 40 ++++- src/pages/settings.vue | 21 ++- src/utils/dataProvider.js | 107 +++++++++++++ src/utils/settings.js | 7 +- 7 files changed, 271 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index 606629f..e993c0b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@mdi/font": "7.4.47", "axios": "^1.7.7", + "idb": "^8.0.2", "roboto-fontface": "*", "vue": "^3.4.31", "vue-masonry-wall": "^0.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61399b2..ed65936 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: axios: specifier: ^1.7.7 version: 1.7.7 + idb: + specifier: ^8.0.2 + version: 8.0.2 roboto-fontface: specifier: '*' version: 0.10.0 @@ -990,6 +993,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + idb@8.0.2: + resolution: {integrity: sha512-CX70rYhx7GDDQzwwQMDwF6kDRQi5vVs6khHUumDrMecBylKkwvZ8HWvKV08AGb7VbpoGCWUQ4aHzNDgoUiOIUg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2752,6 +2758,8 @@ snapshots: dependencies: function-bind: 1.1.2 + idb@8.0.2: {} + ignore@5.3.2: {} immutable@4.3.7: {} diff --git a/src/components/settings/StudentListCard.vue b/src/components/settings/StudentListCard.vue index 527fd98..53ffe03 100644 --- a/src/components/settings/StudentListCard.vue +++ b/src/components/settings/StudentListCard.vue @@ -53,7 +53,7 @@ @@ -131,8 +131,8 @@ export default { name: 'StudentListCard', + props: { modelValue: { type: Object, @@ -240,22 +241,33 @@ export default { data() { return { - newStudent: '', - editingIndex: -1, - editingName: '', - internalOriginalList: [] // 添加内部状态 + newStudentName: '', // 重命名以更清晰 + editState: { + index: -1, + name: '' + }, + savedState: null // 初始为 null,用于判断是否已初始化 } }, created() { - // 初始化内部状态 - this.internalOriginalList = [...this.originalList]; + // 使用 modelValue 初始化保存状态 + this.savedState = { + list: [...this.modelValue.list], + text: this.modelValue.text + } }, watch: { originalList: { handler(newList) { - this.internalOriginalList = [...newList]; + // 仅在首次加载时更新保存状态 + if (!this.savedState || this.savedState.list.length === 0) { + this.savedState = { + list: [...newList], + text: newList.join('\n') + } + } }, immediate: true } @@ -273,60 +285,66 @@ export default { } }, hasChanges() { - const currentList = this.modelValue.list; - const originalList = this.internalOriginalList; + if (!this.savedState) return false; - if (currentList.length !== originalList.length) { - return true; - } - - // 使用 JSON 字符串比较来优化性能 - return JSON.stringify(currentList) !== JSON.stringify(originalList); + const currentState = JSON.stringify({ + list: this.modelValue.list, + text: this.modelValue.text + }); + const savedState = JSON.stringify(this.savedState); + return currentState !== savedState; } }, methods: { + // 初始化方法 + initializeSavedList() { + // 优先使用 modelValue 的当前值,否则使用 originalList + this.savedList = this.modelValue.list.length > 0 + ? [...this.modelValue.list] + : [...this.originalList]; + }, + + // 列表状态检查 + isListChanged(current, original) { + if (current.length !== original.length) return true; + return JSON.stringify(current) !== JSON.stringify(original); + }, + + // 编辑模式切换 toggleAdvanced() { const advanced = !this.modelValue.advanced; - const text = advanced ? this.modelValue.list.join('\n') : this.modelValue.text; - - this.$emit('update:modelValue', { - ...this.modelValue, + this.updateModelValue({ advanced, - text + text: advanced ? this.modelValue.list.join('\n') : this.modelValue.text, + list: this.modelValue.list }); }, - handleTextInput(value) { - const list = value - .split('\n') - .map(s => s.trim()) - .filter(s => s); - + // 更新数据方法 + updateModelValue(newData) { this.$emit('update:modelValue', { ...this.modelValue, - text: value, - list + ...newData }); }, + // 学生管理方法 addStudent() { - const name = this.newStudent.trim(); - if (name && !this.modelValue.list.includes(name)) { - const newList = [...this.modelValue.list, name]; - this.$emit('update:modelValue', { - ...this.modelValue, - list: newList, - text: newList.join('\n') - }); - this.newStudent = ''; - } + const name = this.newStudentName.trim(); + if (!name || this.modelValue.list.includes(name)) return; + + const newList = [...this.modelValue.list, name]; + this.updateModelValue({ + list: newList, + text: newList.join('\n') + }); + this.newStudentName = ''; }, removeStudent(index) { const newList = this.modelValue.list.filter((_, i) => i !== index); - this.$emit('update:modelValue', { - ...this.modelValue, + this.updateModelValue({ list: newList, text: newList.join('\n') }); @@ -348,32 +366,30 @@ export default { const [student] = newList.splice(index, 1); newList.splice(targetIndex, 0, student); - this.$emit('update:modelValue', { - ...this.modelValue, + this.updateModelValue({ list: newList, text: newList.join('\n') }); } }, + // 编辑状态管理 startEdit(index, name) { - this.editingIndex = index; - this.editingName = name; + this.editState = { index, name }; }, saveEdit() { - if (this.editingIndex !== -1 && this.editingName.trim()) { - const newList = [...this.modelValue.list]; - newList[this.editingIndex] = this.editingName.trim(); + const { index, name } = this.editState; + if (index === -1 || !name.trim()) return; - this.$emit('update:modelValue', { - ...this.modelValue, - list: newList, - text: newList.join('\n') - }); - } - this.editingIndex = -1; - this.editingName = ''; + const newList = [...this.modelValue.list]; + newList[index] = name.trim(); + + this.updateModelValue({ + list: newList, + text: newList.join('\n') + }); + this.editState = { index: -1, name: '' }; }, handleClick(index, student) { @@ -383,10 +399,26 @@ export default { } }, + // 保存和加载处理 async handleSave() { await this.$emit('save'); - // 保存成功后更新内部状态 - this.internalOriginalList = [...this.modelValue.list]; + // 保存时更新状态 + this.savedState = { + list: [...this.modelValue.list], + text: this.modelValue.text + }; + }, + + handleTextInput(value) { + const list = value + .split('\n') + .map(s => s.trim()) + .filter(s => s); + + this.updateModelValue({ + text: value, + list: list + }); } } } @@ -403,14 +435,13 @@ export default { } .unsaved-changes { - animation: subtle-pulse 2s infinite; + animation: pulse-warning 2s infinite; /* 更有意义的动画名称 */ border: 2px solid rgb(var(--v-theme-warning)); } -@keyframes subtle-pulse { - 0% { border-color: rgba(var(--v-theme-warning), 1); } +@keyframes pulse-warning { + 0%, 100% { border-color: rgba(var(--v-theme-warning), 1); } 50% { border-color: rgba(var(--v-theme-warning), 0.5); } - 100% { border-color: rgba(var(--v-theme-warning), 1); } } @media (max-width: 600px) { diff --git a/src/pages/index.vue b/src/pages/index.vue index 32ed20e..d23ce28 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -211,6 +211,16 @@ @click="showSyncMessage" > 同步完成 + + 随机点名 {}); + + this.downloadData(); + } } }, diff --git a/src/pages/settings.vue b/src/pages/settings.vue index 92f1d09..db7ae2c 100644 --- a/src/pages/settings.vue +++ b/src/pages/settings.vue @@ -181,6 +181,23 @@ /> + + + + + + 随机点名按钮 + 指向IslandCaller的链接 + + @@ -330,6 +347,7 @@ export default { display: { emptySubjectDisplay: getSetting('display.emptySubjectDisplay'), dynamicSort: getSetting('display.dynamicSort'), + showRandomButton: getSetting('display.showRandomButton') }, developer: { enabled: getSetting('developer.enabled'), @@ -346,7 +364,8 @@ export default { settings, dataProviders: [ { title: '服务器', value: 'server' }, - { title: '本地存储', value: 'localStorage' } + { title: '本地存储', value: 'localStorage' }, + { title:'本地数据库',value:'indexedDB'} ], studentData: { list: [], diff --git a/src/utils/dataProvider.js b/src/utils/dataProvider.js index 1535d07..0ed59ef 100644 --- a/src/utils/dataProvider.js +++ b/src/utils/dataProvider.js @@ -1,4 +1,21 @@ import axios from 'axios'; +import { openDB } from 'idb'; + +const DB_NAME = 'HomeworkDB'; +const DB_VERSION = 1; + +const initDB = async () => { + return openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains('homework')) { + db.createObjectStore('homework'); + } + if (!db.objectStoreNames.contains('config')) { + db.createObjectStore('config'); + } + }, + }); +}; const formatResponse = (data, message = null) => ({ success: true, @@ -157,6 +174,96 @@ const providers = { ); } } + }, + + indexedDB: { + async loadData(key, date) { + try { + const classNumber = key.split('/').pop(); + if (!classNumber) { + return formatError('请先设置班号', 'CONFIG_ERROR'); + } + + const db = await initDB(); + const storageKey = `homework_${classNumber}_${date}`; + const data = await db.get('homework', storageKey); + + if (!data) { + const today = new Date().toISOString().split('T')[0]; + if (date === today) { + return formatResponse({ + homework: {}, + attendance: { absent: [], late: [] } + }); + } + return formatError('数据不存在', 'NOT_FOUND'); + } + + // 从字符串解析数据 + return formatResponse(JSON.parse(data)); + } catch (error) { + return formatError('读取IndexedDB数据失败:' + error); + } + }, + + async saveData(key, data, date) { + try { + const classNumber = key.split('/').pop(); + if (!classNumber) { + return formatError('请先设置班号', 'CONFIG_ERROR'); + } + + const db = await initDB(); + const storageKey = `homework_${classNumber}_${date}`; + // 将数据序列化为字符串存储 + await db.put('homework', JSON.stringify(data), storageKey); + return formatResponse(null, '保存成功'); + } catch (error) { + return formatError('保存IndexedDB数据失败:' + error); + } + }, + + async loadConfig(key) { + try { + const classNumber = key.split('/').pop(); + if (!classNumber) { + return formatError('请先设置班号', 'CONFIG_ERROR'); + } + + const db = await initDB(); + const storageKey = `config_${classNumber}`; + const config = await db.get('config', storageKey); + + if (!config) { + return formatResponse({ + studentList: [], + displayOptions: {} + }); + } + + // 从字符串解析配置 + return formatResponse(JSON.parse(config)); + } catch (error) { + return formatError('读取IndexedDB配置失败:' + error); + } + }, + + async saveConfig(key, config) { + try { + const classNumber = key.split('/').pop(); + if (!classNumber) { + return formatError('请先设置班号', 'CONFIG_ERROR'); + } + + const db = await initDB(); + const storageKey = `config_${classNumber}`; + // 将配置序列化为字符串存储 + await db.put('config', JSON.stringify(config), storageKey); + return formatResponse(null, '保存成功'); + } catch (error) { + return formatError('保存IndexedDB配置失败:' + error); + } + } } }; diff --git a/src/utils/settings.js b/src/utils/settings.js index eff982b..bf516fd 100644 --- a/src/utils/settings.js +++ b/src/utils/settings.js @@ -29,6 +29,11 @@ const settingsDefinitions = { default: true, description: '是否启用动态排序以优化显示效果' }, + 'display.showRandomButton': { + type: 'boolean', + default: false, + description: '是否显示随机按钮' + }, // 服务器设置(合并了数据提供者设置) 'server.domain': { @@ -46,7 +51,7 @@ const settingsDefinitions = { 'server.provider': { // 新增项 type: 'string', default: 'localStorage', - validate: value => ['server', 'localStorage'].includes(value), + validate: value => ['server', 'localStorage', 'indexedDB'].includes(value), description: '数据提供者,用于决定数据存储方式' },