mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-07-04 18:39:22 +00:00
1
This commit is contained in:
parent
08e95a3efc
commit
96c4ab81d9
@ -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",
|
||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
@ -53,7 +53,7 @@
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
v-model="newStudent"
|
||||
v-model="newStudentName"
|
||||
label="添加学生"
|
||||
placeholder="输入学生姓名后回车添加"
|
||||
prepend-inner-icon="mdi-account-plus"
|
||||
@ -67,7 +67,7 @@
|
||||
icon="mdi-plus"
|
||||
variant="text"
|
||||
color="primary"
|
||||
:disabled="!newStudent.trim()"
|
||||
:disabled="!newStudentName.trim()"
|
||||
@click="addStudent"
|
||||
/>
|
||||
</template>
|
||||
@ -131,8 +131,8 @@
|
||||
</v-menu>
|
||||
|
||||
<v-text-field
|
||||
v-if="editingIndex === index"
|
||||
v-model="editingName"
|
||||
v-if="editState.index === index"
|
||||
v-model="editState.name"
|
||||
density="compact"
|
||||
variant="underlined"
|
||||
hide-details
|
||||
@ -219,6 +219,7 @@
|
||||
<script>
|
||||
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 name = this.newStudentName.trim();
|
||||
if (!name || this.modelValue.list.includes(name)) return;
|
||||
|
||||
const newList = [...this.modelValue.list, name];
|
||||
this.$emit('update:modelValue', {
|
||||
...this.modelValue,
|
||||
this.updateModelValue({
|
||||
list: newList,
|
||||
text: newList.join('\n')
|
||||
});
|
||||
this.newStudent = '';
|
||||
}
|
||||
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,
|
||||
const newList = [...this.modelValue.list];
|
||||
newList[index] = name.trim();
|
||||
|
||||
this.updateModelValue({
|
||||
list: newList,
|
||||
text: newList.join('\n')
|
||||
});
|
||||
}
|
||||
this.editingIndex = -1;
|
||||
this.editingName = '';
|
||||
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) {
|
||||
|
@ -211,6 +211,16 @@
|
||||
@click="showSyncMessage"
|
||||
>
|
||||
同步完成
|
||||
</v-btn><v-btn
|
||||
v-if="showRandomButton"
|
||||
color="yellow"
|
||||
prepend-icon="mdi-account-question"
|
||||
append-icon="mdi-dice-multiple"
|
||||
size="large"
|
||||
class="ml-2"
|
||||
href="classisland://plugins/IslandCaller/Run"
|
||||
>
|
||||
随机点名
|
||||
</v-btn>
|
||||
</v-container>
|
||||
<v-dialog
|
||||
@ -540,6 +550,9 @@ export default {
|
||||
},
|
||||
unreadCount() {
|
||||
return this.$refs.messageLog?.unreadCount || 0;
|
||||
},
|
||||
showRandomButton() {
|
||||
return getSetting('display.showRandomButton');
|
||||
}
|
||||
},
|
||||
|
||||
@ -605,10 +618,13 @@ export default {
|
||||
this.dataKey = this.provider === 'server' ? `${domain}/${classNum}` : classNum;
|
||||
this.state.classNumber = classNum;
|
||||
|
||||
const date = new URLSearchParams(window.location.search).get("date")
|
||||
|| new Date().toISOString().split("T")[0];
|
||||
this.state.dateString = date;
|
||||
this.state.isToday = date === new Date().toISOString().split("T")[0];
|
||||
// 从 URL 获取日期,如果没有则使用今天的日期
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dateFromUrl = urlParams.get("date");
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
|
||||
this.state.dateString = dateFromUrl || today;
|
||||
this.state.isToday = this.state.dateString === today;
|
||||
|
||||
await Promise.all([
|
||||
this.downloadData(),
|
||||
@ -816,6 +832,7 @@ export default {
|
||||
|
||||
this.provider = provider;
|
||||
this.dataKey = provider === 'server' ? `${domain}/${classNum}` : classNum;
|
||||
|
||||
this.state.classNumber = classNum;
|
||||
},
|
||||
|
||||
@ -846,10 +863,19 @@ export default {
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const formattedDate = `${year}-${month}-${day}`;
|
||||
|
||||
// 只有当日期真正改变时才更新
|
||||
if (this.state.dateString !== formattedDate) {
|
||||
this.state.dateString = formattedDate;
|
||||
this.$router.push(`/?date=${formattedDate}`);
|
||||
|
||||
// 使用 replace 而不是 push 来避免创建新的历史记录
|
||||
this.$router.replace({
|
||||
query: { date: formattedDate }
|
||||
}).catch(() => {});
|
||||
|
||||
this.downloadData();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
optimizeGridLayout(items) {
|
||||
|
@ -181,6 +181,23 @@
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-dice-multiple" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>随机点名按钮</v-list-item-title>
|
||||
<v-list-item-subtitle>指向IslandCaller的链接</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="settings.display.showRandomButton"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</settings-card>
|
||||
</v-col>
|
||||
@ -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: [],
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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: '数据提供者,用于决定数据存储方式'
|
||||
},
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user