1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-04 18:39:22 +00:00
This commit is contained in:
SunWuyuan 2025-03-09 14:59:35 +08:00
parent 08e95a3efc
commit 96c4ab81d9
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
7 changed files with 271 additions and 74 deletions

View File

@ -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
View File

@ -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: {}

View File

@ -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 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) {

View File

@ -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,9 +863,18 @@ export default {
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const formattedDate = `${year}-${month}-${day}`;
this.state.dateString = formattedDate;
this.$router.push(`/?date=${formattedDate}`);
this.downloadData();
//
if (this.state.dateString !== formattedDate) {
this.state.dateString = formattedDate;
// 使 replace push
this.$router.replace({
query: { date: formattedDate }
}).catch(() => {});
this.downloadData();
}
}
},

View File

@ -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: [],

View File

@ -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);
}
}
}
};

View File

@ -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: '数据提供者,用于决定数据存储方式'
},