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-14 21:53:22 +08:00
parent ebf3e9df94
commit 9bb3f06ba1
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
4 changed files with 204 additions and 116 deletions

View File

@ -0,0 +1,41 @@
<template>
<div class="warning-container">
<v-chip
v-if="show"
color="warning"
size="small"
class="warning-chip"
>
{{ message }}
</v-chip>
</div>
</template>
<script>
export default {
name: 'UnsavedWarning',
props: {
show: Boolean,
message: {
type: String,
default: '未保存'
}
}
}
</script>
<style scoped>
.warning-container {
display: inline-block;
margin-right: 8px;
}
.warning-chip {
animation: fade-in 0.3s ease;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@ -10,14 +10,10 @@
</template> </template>
<v-card-title class="text-h6">学生列表</v-card-title> <v-card-title class="text-h6">学生列表</v-card-title>
<template #append> <template #append>
<v-chip <unsaved-warning
v-if="hasChanges" :show="hasChanges"
color="warning" message="有未保存的更改"
size="small" />
class="mr-2"
>
未保存
</v-chip>
<v-btn <v-btn
:color="modelValue.advanced ? 'primary' : undefined" :color="modelValue.advanced ? 'primary' : undefined"
variant="text" variant="text"
@ -217,9 +213,14 @@
</template> </template>
<script> <script>
import UnsavedWarning from '../common/UnsavedWarning.vue'
import '@/styles/warnings.scss'
export default { export default {
name: 'StudentListCard', name: 'StudentListCard',
components: {
UnsavedWarning
},
props: { props: {
modelValue: { modelValue: {
type: Object, type: Object,
@ -246,15 +247,12 @@ export default {
index: -1, index: -1,
name: '' name: ''
}, },
savedState: { // null savedState: null // null
list: [],
text: ''
}
} }
}, },
created() { created() {
// mounted this.initializeSavedState()
}, },
mounted() { mounted() {
@ -300,23 +298,17 @@ export default {
} }
}, },
hasChanges() { hasChanges() {
const currentState = JSON.stringify({ return this.savedState && this.isStateChanged();
list: this.modelValue.list,
text: this.modelValue.text
});
const savedState = JSON.stringify(this.savedState);
// savedState
return this.savedState.list.length > 0 && currentState !== savedState;
} }
}, },
methods: { methods: {
// //
initializeSavedList() { initializeSavedState() {
// 使 modelValue 使 originalList this.savedState = {
this.savedList = this.modelValue.list.length > 0 list: [...(this.modelValue.list.length ? this.modelValue.list : this.originalList)],
? [...this.modelValue.list] text: this.modelValue.text || (this.originalList || []).join('\n')
: [...this.originalList]; }
}, },
// //
@ -349,6 +341,7 @@ export default {
list: [...this.modelValue.list], list: [...this.modelValue.list],
text: this.modelValue.text text: this.modelValue.text
}; };
this.$forceUpdate(); //
}, },
// //
@ -428,6 +421,7 @@ export default {
this.updateSavedState(); this.updateSavedState();
} catch (error) { } catch (error) {
console.error('保存失败:', error); console.error('保存失败:', error);
throw error;
} }
}, },
@ -441,6 +435,18 @@ export default {
text: value, text: value,
list list
}); });
},
//
isStateChanged() {
if (!this.savedState) return false;
const currentState = {
list: this.modelValue.list,
text: this.modelValue.text
};
return JSON.stringify(currentState) !== JSON.stringify(this.savedState);
} }
} }
} }
@ -456,16 +462,22 @@ export default {
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
.unsaved-changes { /* 修改警告样式的选择器和实现 */
animation: pulse-warning 2s infinite; /* 更有意义的动画名称 */ .v-card.unsaved-changes {
border: 2px solid rgb(var(--v-theme-warning)); animation: pulse-warning 2s infinite;
border: 2px solid rgb(var(--v-theme-warning)) !important;
} }
@keyframes pulse-warning { @keyframes pulse-warning {
0%, 100% { border-color: rgba(var(--v-theme-warning), 1); } 0%, 100% {
50% { border-color: rgba(var(--v-theme-warning), 0.5); } border-color: rgba(var(--v-theme-warning), 1) !important;
}
50% {
border-color: rgba(var(--v-theme-warning), 0.5) !important;
}
} }
/* 移动端样式 */
@media (max-width: 600px) { @media (max-width: 600px) {
.action-buttons { .action-buttons {
opacity: 1; opacity: 1;

View File

@ -47,7 +47,7 @@
@click="$refs.messageLog.drawer = true" @click="$refs.messageLog.drawer = true"
/> />
</template> </template>
</v-app-bar> </v-app-bar>{{ state.boardData }}
<div class="d-flex"> <div class="d-flex">
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<v-container class="main-window flex-grow-1 no-select" fluid> <v-container class="main-window flex-grow-1 no-select" fluid>
@ -133,27 +133,27 @@
@click="setAttendanceArea()" @click="setAttendanceArea()"
> >
<h1>出勤</h1> <h1>出勤</h1>
<h2>应到: {{ state.studentList.length - state.excludeSet.size }}</h2> <h2>应到: {{ state.studentList.length - state.boardData.attendance.exclude.length }}</h2>
<h2> <h2>
实到: 实到:
{{ {{
state.studentList.length - state.studentList.length -
state.selectedSet.size - state.boardData.attendance.absent.length -
state.lateSet.size - state.boardData.attendance.late.length -
state.excludeSet.size state.boardData.attendance.exclude.length
}} }}
</h2> </h2>
<h2>请假: {{ state.selectedSet.size }}</h2> <h2>请假: {{ state.boardData.attendance.absent.length }}</h2>
<h3 v-for="(i, index) in state.selectedSet" :key="'absent-' + index"> <h3 v-for="(name, index) in state.boardData.attendance.absent" :key="'absent-' + index">
{{ `${index + 1}. ${state.studentList[i]}` }} {{ `${index + 1}. ${name}` }}
</h3> </h3>
<h2>迟到: {{ state.lateSet.size }}</h2> <h2>迟到: {{ state.boardData.attendance.late.length }}</h2>
<h3 v-for="(i, index) in state.lateSet" :key="'late-' + index"> <h3 v-for="(name, index) in state.boardData.attendance.late" :key="'late-' + index">
{{ `${index + 1}. ${state.studentList[i]}` }} {{ `${index + 1}. ${name}` }}
</h3> </h3>
<h2>不参与: {{ state.excludeSet.size }}</h2> <h2>不参与: {{ state.boardData.attendance.exclude.length }}</h2>
<h3 v-for="(i, index) in state.excludeSet" :key="'exclude-' + index"> <h3 v-for="(name, index) in state.boardData.attendance.exclude" :key="'exclude-' + index">
{{ `${index + 1}. ${state.studentList[i]}` }} {{ `${index + 1}. ${name}` }}
</h3> </h3>
</v-col> </v-col>
</div> </div>
@ -314,9 +314,14 @@ export default {
state: { state: {
classNumber: "", classNumber: "",
studentList: [], studentList: [],
selectedSet: new Set(), boardData: {
lateSet: new Set(), homework: {},
excludeSet: new Set(), // attendance: {
absent: [],
late: [],
exclude: []
}
},
dialogVisible: false, dialogVisible: false,
dialogTitle: "", dialogTitle: "",
textarea: "", textarea: "",
@ -326,7 +331,6 @@ export default {
contentStyle: { "font-size": `${getSetting("font.size")}px` }, contentStyle: { "font-size": `${getSetting("font.size")}px` },
uploadLoading: false, uploadLoading: false,
downloadLoading: false, downloadLoading: false,
homeworkData: {},
snackbar: false, snackbar: false,
snackbarText: "", snackbarText: "",
fontSize: getSetting("font.size"), fontSize: getSetting("font.size"),
@ -403,13 +407,13 @@ export default {
}, },
sortedItems() { sortedItems() {
const key = `${JSON.stringify( const key = `${JSON.stringify(
this.state.homeworkData this.state.boardData.homework
)}_${this.state.subjectOrder.join()}_${this.dynamicSort}`; )}_${this.state.subjectOrder.join()}_${this.dynamicSort}`;
if (this.sortedItemsCache.key === key) { if (this.sortedItemsCache.key === key) {
return this.sortedItemsCache.value; return this.sortedItemsCache.value;
} }
const items = Object.entries(this.state.homeworkData) const items = Object.entries(this.state.boardData.homework)
.filter(([, value]) => value.content?.trim()) .filter(([, value]) => value.content?.trim())
.map(([key, value]) => ({ .map(([key, value]) => ({
key, key,
@ -433,7 +437,7 @@ export default {
return result; return result;
}, },
unusedSubjects() { unusedSubjects() {
const usedKeys = Object.keys(this.state.homeworkData); const usedKeys = Object.keys(this.state.boardData.homework);
return this.state.availableSubjects.filter( return this.state.availableSubjects.filter(
(subject) => !usedKeys.includes(subject.key) (subject) => !usedKeys.includes(subject.key)
); );
@ -555,20 +559,16 @@ export default {
if (response.error.code === "NOT_FOUND") { if (response.error.code === "NOT_FOUND") {
this.state.showNoDataMessage = true; this.state.showNoDataMessage = true;
this.state.noDataMessage = response.error.message; this.state.noDataMessage = response.error.message;
this.state.homeworkData = {}; this.state.boardData = {
this.state.selectedSet = new Set(); homework: {},
this.state.lateSet = new Set(); attendance: { absent: [], late: [], exclude: [] }
this.state.excludeSet = new Set(); // };
} else { } else {
throw new Error(response.error.message); throw new Error(response.error.message);
} }
} else { } else {
// //
const { homework = {}, attendance = {} } = response.data; this.state.boardData = response.data;
this.state.homeworkData = homework;
this.state.selectedSet = new Set(attendance.absent || []);
this.state.lateSet = new Set(attendance.late || []);
this.state.excludeSet = new Set(attendance.exclude || []); //
this.state.synced = true; this.state.synced = true;
this.state.showNoDataMessage = false; this.state.showNoDataMessage = false;
this.showMessage("下载成功", "数据已更新"); this.showMessage("下载成功", "数据已更新");
@ -588,14 +588,7 @@ export default {
const response = await dataProvider.saveData( const response = await dataProvider.saveData(
this.provider, this.provider,
this.dataKey, this.dataKey,
{ this.state.boardData,
homework: this.state.homeworkData,
attendance: {
absent: Array.from(this.state.selectedSet),
late: Array.from(this.state.lateSet),
exclude: Array.from(this.state.excludeSet), //
},
},
this.state.dateString this.state.dateString
); );
@ -636,7 +629,7 @@ export default {
const content = this.state.textarea.trim(); const content = this.state.textarea.trim();
if (content) { if (content) {
this.state.homeworkData[this.currentEditSubject] = { this.state.boardData.homework[this.currentEditSubject] = {
name: this.state.availableSubjects.find( name: this.state.availableSubjects.find(
(s) => s.key === this.currentEditSubject (s) => s.key === this.currentEditSubject
)?.name, )?.name,
@ -648,7 +641,7 @@ export default {
this.uploadData(); this.uploadData();
} }
} else { } else {
delete this.state.homeworkData[this.currentEditSubject]; delete this.state.boardData.homework[this.currentEditSubject];
} }
this.state.dialogVisible = false; this.state.dialogVisible = false;
}, },
@ -674,16 +667,16 @@ export default {
} }
this.currentEditSubject = subject; this.currentEditSubject = subject;
// //
if (!this.state.homeworkData[subject]) { if (!this.state.boardData.homework[subject]) {
this.state.homeworkData[subject] = { this.state.boardData.homework[subject] = {
name: name:
this.state.availableSubjects.find((s) => s.key === subject)?.name || this.state.availableSubjects.find((s) => s.key === subject)?.name ||
subject, subject,
content: "", content: "",
}; };
} }
this.state.dialogTitle = this.state.homeworkData[subject].name; this.state.dialogTitle = this.state.boardData.homework[subject].name;
this.state.textarea = this.state.homeworkData[subject].content; this.state.textarea = this.state.boardData.homework[subject].content;
this.state.dialogVisible = true; this.state.dialogVisible = true;
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.inputRef) { if (this.$refs.inputRef) {
@ -701,16 +694,17 @@ export default {
}, },
toggleStudentStatus(index) { toggleStudentStatus(index) {
if (this.state.selectedSet.has(index)) { const student = this.state.studentList[index];
this.state.selectedSet.delete(index); if (this.state.boardData.attendance.absent.includes(student)) {
this.state.lateSet.add(index); this.state.boardData.attendance.absent = this.state.boardData.attendance.absent.filter(name => name !== student);
} else if (this.state.lateSet.has(index)) { this.state.boardData.attendance.late.push(student);
this.state.lateSet.delete(index); } else if (this.state.boardData.attendance.late.includes(student)) {
this.state.excludeSet.add(index); this.state.boardData.attendance.late = this.state.boardData.attendance.late.filter(name => name !== student);
} else if (this.state.excludeSet.has(index)) { this.state.boardData.attendance.exclude.push(student);
this.state.excludeSet.delete(index); } else if (this.state.boardData.attendance.exclude.includes(student)) {
this.state.boardData.attendance.exclude = this.state.boardData.attendance.exclude.filter(name => name !== student);
} else { } else {
this.state.selectedSet.add(index); this.state.boardData.attendance.absent.push(student);
} }
this.state.synced = false; this.state.synced = false;
if (this.autoSave) { if (this.autoSave) {
@ -719,9 +713,9 @@ export default {
}, },
cleanstudentslist() { cleanstudentslist() {
this.state.selectedSet.clear(); this.state.boardData.attendance.absent = [];
this.state.lateSet.clear(); this.state.boardData.attendance.late = [];
this.state.excludeSet.clear(); this.state.boardData.attendance.exclude = [];
this.state.synced = false; this.state.synced = false;
if (this.autoSave) { if (this.autoSave) {
this.uploadData(); this.uploadData();
@ -865,65 +859,80 @@ export default {
}, },
setAllPresent() { setAllPresent() {
this.state.selectedSet.clear(); this.state.boardData.attendance = {
this.state.lateSet.clear(); absent: [],
this.state.excludeSet.clear(); late: [],
exclude: []
};
this.state.synced = false;
}, },
setAllAbsent() { setAllAbsent() {
this.state.studentList.forEach((_, index) => { this.state.boardData.attendance.absent = [...this.state.studentList];
this.setAbsent(index); this.state.boardData.attendance.late = [];
}); this.state.boardData.attendance.exclude = [];
this.state.synced = false;
}, },
setAllLate() { setAllLate() {
this.state.studentList.forEach((_, index) => { this.state.boardData.attendance.absent = [];
this.setLate(index); this.state.boardData.attendance.late = [...this.state.studentList];
}); this.state.boardData.attendance.exclude = [];
this.state.synced = false;
}, },
isPresent(index) { isPresent(index) {
return ( const student = this.state.studentList[index];
!this.state.selectedSet.has(index) && const { absent, late, exclude } = this.state.boardData.attendance;
!this.state.lateSet.has(index) && return !absent.includes(student) && !late.includes(student) && !exclude.includes(student);
!this.state.excludeSet.has(index)
);
}, },
isAbsent(index) { isAbsent(index) {
return this.state.selectedSet.has(index); return this.state.boardData.attendance.absent.includes(this.state.studentList[index]);
}, },
isLate(index) { isLate(index) {
return this.state.lateSet.has(index); return this.state.boardData.attendance.late.includes(this.state.studentList[index]);
}, },
isExclude(index) { isExclude(index) {
return this.state.excludeSet.has(index); return this.state.boardData.attendance.exclude.includes(this.state.studentList[index]);
}, },
setPresent(index) { setPresent(index) {
this.state.selectedSet.delete(index); const student = this.state.studentList[index];
this.state.lateSet.delete(index); const { absent, late, exclude } = this.state.boardData.attendance;
this.state.excludeSet.delete(index); this.state.boardData.attendance.absent = absent.filter(name => name !== student);
this.state.boardData.attendance.late = late.filter(name => name !== student);
this.state.boardData.attendance.exclude = exclude.filter(name => name !== student);
this.state.synced = false;
}, },
setAbsent(index) { setAbsent(index) {
this.state.selectedSet.add(index); const student = this.state.studentList[index];
this.state.lateSet.delete(index); if (!this.state.boardData.attendance.absent.includes(student)) {
this.state.excludeSet.delete(index); this.setPresent(index);
this.state.boardData.attendance.absent.push(student);
this.state.synced = false;
}
}, },
setLate(index) { setLate(index) {
this.state.lateSet.add(index); const student = this.state.studentList[index];
this.state.selectedSet.delete(index); if (!this.state.boardData.attendance.late.includes(student)) {
this.state.excludeSet.delete(index); this.setPresent(index);
this.state.boardData.attendance.late.push(student);
this.state.synced = false;
}
}, },
setExclude(index) { setExclude(index) {
this.state.excludeSet.add(index); const student = this.state.studentList[index];
this.state.selectedSet.delete(index); if (!this.state.boardData.attendance.exclude.includes(student)) {
this.state.lateSet.delete(index); this.setPresent(index);
this.state.boardData.attendance.exclude.push(student);
this.state.synced = false;
}
}, },
async saveAttendance() { async saveAttendance() {

26
src/styles/warnings.scss Normal file
View File

@ -0,0 +1,26 @@
@mixin warning-card {
&.warning {
animation: pulse-warning 2s infinite;
position: relative;
&::before {
content: '';
position: absolute;
inset: -2px;
border: 2px solid rgb(var(--v-theme-warning));
border-radius: inherit;
animation: pulse-border 2s infinite;
pointer-events: none;
}
}
}
@keyframes pulse-warning {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.002); }
}
@keyframes pulse-border {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}