mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2025-12-07 21:13:11 +00:00
feat: Add attendance management dialog and sidebar components
- Implemented AttendanceManagementDialog.vue for managing student attendance with search and filter functionalities. - Created AttendanceSidebar.vue to display attendance statistics and lists of students by status. - Introduced HomeActions.vue for various actions including upload and random picker. - Developed HomeworkGrid.vue to display homework and attendance cards in a masonry layout. - Added glow effect styles in glow.scss for enhanced UI interactions. - Updated index.scss to include glow styles.
This commit is contained in:
parent
b3595422c7
commit
8d9b9a3f32
@ -1,8 +1,18 @@
|
|||||||
# 创建新的作业编辑对话框组件
|
# 创建新的作业编辑对话框组件
|
||||||
<template>
|
<template>
|
||||||
<v-dialog v-model="dialogVisible" max-width="900" width="auto" @click:outside="handleClose">
|
<v-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:fullscreen="isMobile"
|
||||||
|
max-width="900"
|
||||||
|
width="auto"
|
||||||
|
@click:outside="handleClose"
|
||||||
|
>
|
||||||
<v-card border>
|
<v-card border>
|
||||||
<v-card-title>{{ title }}</v-card-title>
|
<v-card-title class="d-flex align-center">
|
||||||
|
{{ title }}
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn icon="mdi-close" variant="text" @click="handleClose" />
|
||||||
|
</v-card-title>
|
||||||
<v-card-subtitle>
|
<v-card-subtitle>
|
||||||
{{ autoSave ? autoSavePromptText : manualSavePromptText }}
|
{{ autoSave ? autoSavePromptText : manualSavePromptText }}
|
||||||
</v-card-subtitle>
|
</v-card-subtitle>
|
||||||
@ -15,7 +25,7 @@
|
|||||||
auto-grow
|
auto-grow
|
||||||
placeholder="使用换行表示分条"
|
placeholder="使用换行表示分条"
|
||||||
rows="5"
|
rows="5"
|
||||||
width="480"
|
:width="isMobile ? '100%' : '480'"
|
||||||
@click="updateCurrentLine"
|
@click="updateCurrentLine"
|
||||||
@keyup="updateCurrentLine"
|
@keyup="updateCurrentLine"
|
||||||
/>
|
/>
|
||||||
@ -102,7 +112,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Tools Section -->
|
<!-- Quick Tools Section -->
|
||||||
<div v-if="showQuickTools" class="quick-tools ml-4" style="min-width: 180px;">
|
<div v-if="showQuickTools && !isMobile" class="quick-tools ml-4" style="min-width: 180px;">
|
||||||
<!-- Numeric Keypad -->
|
<!-- Numeric Keypad -->
|
||||||
<div class="numeric-keypad mb-4">
|
<div class="numeric-keypad mb-4">
|
||||||
<div class="keypad-row">
|
<div class="keypad-row">
|
||||||
@ -214,6 +224,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import dataProvider from "@/utils/dataProvider";
|
import dataProvider from "@/utils/dataProvider";
|
||||||
import {getSetting} from "@/utils/settings";
|
import {getSetting} from "@/utils/settings";
|
||||||
|
import { useDisplay } from "vuetify";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "HomeworkEditDialog",
|
name: "HomeworkEditDialog",
|
||||||
@ -236,6 +247,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue", "save"],
|
emits: ["update:modelValue", "save"],
|
||||||
|
setup() {
|
||||||
|
const { mobile } = useDisplay();
|
||||||
|
return { isMobile: mobile };
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
content: "",
|
content: "",
|
||||||
|
|||||||
428
src/components/attendance/AttendanceManagementDialog.vue
Normal file
428
src/components/attendance/AttendanceManagementDialog.vue
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
:fullscreen="isMobile"
|
||||||
|
fullscreen-breakpoint="sm"
|
||||||
|
max-width="900"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2" icon="mdi-account-group" />
|
||||||
|
出勤状态管理
|
||||||
|
<v-spacer />
|
||||||
|
<v-chip v-if="!isMobile" class="ml-2" color="primary" size="small">
|
||||||
|
{{ dateString }}
|
||||||
|
</v-chip>
|
||||||
|
<v-btn v-if="isMobile" icon="mdi-close" variant="text" @click="$emit('update:modelValue', false)" />
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<!-- 批量操作和搜索 -->
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col cols="12" md="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="attendanceSearch"
|
||||||
|
clearable
|
||||||
|
hint="支持筛选姓氏,如输入'孙'可筛选所有姓孙的学生"
|
||||||
|
label="搜索学生"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 姓氏筛选 -->
|
||||||
|
<div class="d-flex flex-wrap mt-2 gap-1">
|
||||||
|
<v-btn
|
||||||
|
v-for="surname in extractedSurnames"
|
||||||
|
:key="surname.name"
|
||||||
|
:color="attendanceSearch === surname.name ? 'primary' : ''"
|
||||||
|
:variant="
|
||||||
|
attendanceSearch === surname.name ? 'elevated' : 'text'
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
attendanceSearch =
|
||||||
|
attendanceSearch === surname.name ? '' : surname.name
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ surname.name }}
|
||||||
|
({{ surname.count }})
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- 过滤器 -->
|
||||||
|
<div class="d-flex flex-wrap mb-4 gap-2">
|
||||||
|
<div>
|
||||||
|
<v-chip
|
||||||
|
:append-icon="
|
||||||
|
attendanceFilter.includes('present') ? 'mdi-check' : ''
|
||||||
|
"
|
||||||
|
:color="attendanceFilter.includes('present') ? 'success' : ''"
|
||||||
|
:variant="
|
||||||
|
attendanceFilter.includes('present') ? 'elevated' : 'tonal'
|
||||||
|
"
|
||||||
|
class="px-2 filter-chip"
|
||||||
|
prepend-icon="mdi-account-check"
|
||||||
|
value="present"
|
||||||
|
@click="toggleFilter('present')"
|
||||||
|
>
|
||||||
|
到课
|
||||||
|
</v-chip>
|
||||||
|
|
||||||
|
<v-chip
|
||||||
|
:append-icon="
|
||||||
|
attendanceFilter.includes('absent') ? 'mdi-check' : ''
|
||||||
|
"
|
||||||
|
:color="attendanceFilter.includes('absent') ? 'error' : ''"
|
||||||
|
:variant="
|
||||||
|
attendanceFilter.includes('absent') ? 'elevated' : 'tonal'
|
||||||
|
"
|
||||||
|
class="px-2 filter-chip"
|
||||||
|
prepend-icon="mdi-account-off"
|
||||||
|
value="absent"
|
||||||
|
@click="toggleFilter('absent')"
|
||||||
|
>
|
||||||
|
请假
|
||||||
|
</v-chip>
|
||||||
|
<v-chip
|
||||||
|
:append-icon="
|
||||||
|
attendanceFilter.includes('late') ? 'mdi-check' : ''
|
||||||
|
"
|
||||||
|
:color="attendanceFilter.includes('late') ? 'warning' : ''"
|
||||||
|
:variant="
|
||||||
|
attendanceFilter.includes('late') ? 'elevated' : 'tonal'
|
||||||
|
"
|
||||||
|
class="px-2 filter-chip"
|
||||||
|
prepend-icon="mdi-clock-alert"
|
||||||
|
value="late"
|
||||||
|
@click="toggleFilter('late')"
|
||||||
|
>
|
||||||
|
迟到
|
||||||
|
</v-chip>
|
||||||
|
<v-chip
|
||||||
|
:append-icon="
|
||||||
|
attendanceFilter.includes('exclude') ? 'mdi-check' : ''
|
||||||
|
"
|
||||||
|
:color="attendanceFilter.includes('exclude') ? 'grey' : ''"
|
||||||
|
:variant="
|
||||||
|
attendanceFilter.includes('exclude') ? 'elevated' : 'tonal'
|
||||||
|
"
|
||||||
|
class="px-2 filter-chip"
|
||||||
|
prepend-icon="mdi-account-cancel"
|
||||||
|
value="exclude"
|
||||||
|
@click="toggleFilter('exclude')"
|
||||||
|
>
|
||||||
|
不参与
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 学生列表 -->
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-for="student in filteredStudents"
|
||||||
|
:key="student"
|
||||||
|
cols="12"
|
||||||
|
lg="4"
|
||||||
|
md="6"
|
||||||
|
sm="6"
|
||||||
|
>
|
||||||
|
<v-card border class="student-card">
|
||||||
|
<v-card-text class="d-flex align-center pa-2">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-avatar
|
||||||
|
:color="getStudentStatusColor(student)"
|
||||||
|
class="mr-2"
|
||||||
|
size="24"
|
||||||
|
>
|
||||||
|
<v-icon size="small"
|
||||||
|
>{{ getStudentStatusIcon(student) }}
|
||||||
|
</v-icon>
|
||||||
|
</v-avatar>
|
||||||
|
<div class="text-subtitle-1">{{ student }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="attendance-actions">
|
||||||
|
<v-btn
|
||||||
|
:color="isPresent(student) ? 'success' : ''"
|
||||||
|
:title="'设为到课'"
|
||||||
|
icon="mdi-account-check"
|
||||||
|
:size="isMobile ? 'default' : 'small'"
|
||||||
|
variant="text"
|
||||||
|
@click="setPresent(student)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
:color="isAbsent(student) ? 'error' : ''"
|
||||||
|
:title="'设为请假'"
|
||||||
|
icon="mdi-account-off"
|
||||||
|
:size="isMobile ? 'default' : 'small'"
|
||||||
|
variant="text"
|
||||||
|
@click="setAbsent(student)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
:color="isLate(student) ? 'warning' : ''"
|
||||||
|
:title="'设为迟到'"
|
||||||
|
icon="mdi-clock-alert"
|
||||||
|
:size="isMobile ? 'default' : 'small'"
|
||||||
|
variant="text"
|
||||||
|
@click="setLate(student)"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
:color="isExclude(student) ? 'grey' : ''"
|
||||||
|
:title="'设为不参与'"
|
||||||
|
icon="mdi-account-cancel"
|
||||||
|
:size="isMobile ? 'default' : 'small'"
|
||||||
|
variant="text"
|
||||||
|
@click="setExclude(student)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="12">
|
||||||
|
<v-card class="mb-4" color="primary" variant="tonal">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-subtitle-2 mb-2">批量操作</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<v-btn
|
||||||
|
class="flex-grow-1"
|
||||||
|
color="success"
|
||||||
|
prepend-icon="mdi-account-check"
|
||||||
|
@click="setAllPresent"
|
||||||
|
>
|
||||||
|
全部到齐
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="flex-grow-1"
|
||||||
|
color="error"
|
||||||
|
prepend-icon="mdi-account-off"
|
||||||
|
@click="setAllAbsent"
|
||||||
|
>
|
||||||
|
全部请假
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="flex-grow-1"
|
||||||
|
color="warning"
|
||||||
|
prepend-icon="mdi-clock-alert"
|
||||||
|
@click="setAllLate"
|
||||||
|
>
|
||||||
|
全部迟到
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="flex-grow-1"
|
||||||
|
color="grey"
|
||||||
|
prepend-icon="mdi-account-cancel"
|
||||||
|
@click="setAllExclude"
|
||||||
|
>
|
||||||
|
全部不参与
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<v-btn color="primary" @click="$emit('save')">
|
||||||
|
<v-icon start>mdi-content-save</v-icon>
|
||||||
|
保存
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { pinyin } from "pinyin-pro";
|
||||||
|
import { useDisplay } from "vuetify";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AttendanceManagementDialog",
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
studentList: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
attendance: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
dateString: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["update:modelValue", "save", "change"],
|
||||||
|
setup() {
|
||||||
|
const { mobile } = useDisplay();
|
||||||
|
return { isMobile: mobile };
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
attendanceSearch: "",
|
||||||
|
attendanceFilter: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredStudents() {
|
||||||
|
let students = [...this.studentList];
|
||||||
|
|
||||||
|
if (this.attendanceSearch) {
|
||||||
|
const searchTerm = this.attendanceSearch.toLowerCase();
|
||||||
|
students = students.filter((student) =>
|
||||||
|
student.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.attendanceFilter && this.attendanceFilter.length > 0) {
|
||||||
|
students = students.filter((student) => {
|
||||||
|
if (this.attendanceFilter.includes("present") && this.isPresent(student))
|
||||||
|
return true;
|
||||||
|
if (this.attendanceFilter.includes("absent") && this.isAbsent(student))
|
||||||
|
return true;
|
||||||
|
if (this.attendanceFilter.includes("late") && this.isLate(student))
|
||||||
|
return true;
|
||||||
|
if (this.attendanceFilter.includes("exclude") && this.isExclude(student))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return students;
|
||||||
|
},
|
||||||
|
extractedSurnames() {
|
||||||
|
if (!this.studentList || this.studentList.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const surnameMap = new Map();
|
||||||
|
|
||||||
|
this.studentList.forEach((student) => {
|
||||||
|
if (student && student.length > 0) {
|
||||||
|
const surname = student.charAt(0);
|
||||||
|
surnameMap.set(surname, (surnameMap.get(surname) || 0) + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(surnameMap.entries())
|
||||||
|
.map(([name, count]) => ({ name, count }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const pinyinA = pinyin(a.name, { toneType: "none" });
|
||||||
|
const pinyinB = pinyin(b.name, { toneType: "none" });
|
||||||
|
return pinyinA.localeCompare(pinyinB);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleFilter(filter) {
|
||||||
|
const index = this.attendanceFilter.indexOf(filter);
|
||||||
|
if (index === -1) {
|
||||||
|
this.attendanceFilter.push(filter);
|
||||||
|
} else {
|
||||||
|
this.attendanceFilter.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isPresent(student) {
|
||||||
|
const { absent, late, exclude } = this.attendance;
|
||||||
|
return (
|
||||||
|
!absent.includes(student) &&
|
||||||
|
!late.includes(student) &&
|
||||||
|
!exclude.includes(student)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isAbsent(student) {
|
||||||
|
return this.attendance.absent.includes(student);
|
||||||
|
},
|
||||||
|
isLate(student) {
|
||||||
|
return this.attendance.late.includes(student);
|
||||||
|
},
|
||||||
|
isExclude(student) {
|
||||||
|
return this.attendance.exclude.includes(student);
|
||||||
|
},
|
||||||
|
getStudentStatusColor(student) {
|
||||||
|
if (this.attendance.absent.includes(student)) return "error";
|
||||||
|
if (this.attendance.late.includes(student)) return "warning";
|
||||||
|
if (this.attendance.exclude.includes(student)) return "grey";
|
||||||
|
return "success";
|
||||||
|
},
|
||||||
|
getStudentStatusIcon(student) {
|
||||||
|
if (this.attendance.absent.includes(student)) return "mdi-account-off";
|
||||||
|
if (this.attendance.late.includes(student)) return "mdi-clock-alert";
|
||||||
|
if (this.attendance.exclude.includes(student)) return "mdi-account-cancel";
|
||||||
|
return "mdi-account-check";
|
||||||
|
},
|
||||||
|
removeFromAll(student) {
|
||||||
|
const idxAbsent = this.attendance.absent.indexOf(student);
|
||||||
|
if (idxAbsent > -1) this.attendance.absent.splice(idxAbsent, 1);
|
||||||
|
|
||||||
|
const idxLate = this.attendance.late.indexOf(student);
|
||||||
|
if (idxLate > -1) this.attendance.late.splice(idxLate, 1);
|
||||||
|
|
||||||
|
const idxExclude = this.attendance.exclude.indexOf(student);
|
||||||
|
if (idxExclude > -1) this.attendance.exclude.splice(idxExclude, 1);
|
||||||
|
},
|
||||||
|
setPresent(student) {
|
||||||
|
this.removeFromAll(student);
|
||||||
|
this.$emit("change");
|
||||||
|
},
|
||||||
|
setAbsent(student) {
|
||||||
|
this.removeFromAll(student);
|
||||||
|
this.attendance.absent.push(student);
|
||||||
|
this.$emit("change");
|
||||||
|
},
|
||||||
|
setLate(student) {
|
||||||
|
this.removeFromAll(student);
|
||||||
|
this.attendance.late.push(student);
|
||||||
|
this.$emit("change");
|
||||||
|
},
|
||||||
|
setExclude(student) {
|
||||||
|
this.removeFromAll(student);
|
||||||
|
this.attendance.exclude.push(student);
|
||||||
|
this.$emit("change");
|
||||||
|
},
|
||||||
|
setAllPresent() {
|
||||||
|
this.attendance.absent.splice(0, this.attendance.absent.length);
|
||||||
|
this.attendance.late.splice(0, this.attendance.late.length);
|
||||||
|
this.attendance.exclude.splice(0, this.attendance.exclude.length);
|
||||||
|
this.$emit("change");
|
||||||
|
},
|
||||||
|
setAllAbsent() {
|
||||||
|
this.setAllPresent(); // Clear first
|
||||||
|
this.attendance.absent.push(...this.studentList);
|
||||||
|
this.$emit("change");
|
||||||
|
},
|
||||||
|
setAllLate() {
|
||||||
|
this.setAllPresent(); // Clear first
|
||||||
|
this.attendance.late.push(...this.studentList);
|
||||||
|
this.$emit("change");
|
||||||
|
},
|
||||||
|
setAllExclude() {
|
||||||
|
this.setAllPresent(); // Clear first
|
||||||
|
this.attendance.exclude.push(...this.studentList);
|
||||||
|
this.$emit("change");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gap-1 {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.gap-2 {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
108
src/components/attendance/AttendanceSidebar.vue
Normal file
108
src/components/attendance/AttendanceSidebar.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<v-col
|
||||||
|
v-if="studentList && studentList.length"
|
||||||
|
v-ripple="{
|
||||||
|
class: `text-${
|
||||||
|
['primary', 'secondary', 'info', 'success', 'warning', 'error'][
|
||||||
|
Math.floor(Math.random() * 6)
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
}"
|
||||||
|
class="attendance-area no-select"
|
||||||
|
cols="1"
|
||||||
|
@click="$emit('click')"
|
||||||
|
>
|
||||||
|
<h1>出勤</h1>
|
||||||
|
<h2>
|
||||||
|
<span style="white-space: nowrap"> 应到</span>
|
||||||
|
:
|
||||||
|
<span style="white-space: nowrap">
|
||||||
|
{{ studentList.length - attendance.exclude.length }}人
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<h2>
|
||||||
|
<span style="white-space: nowrap"> 实到</span>
|
||||||
|
:
|
||||||
|
<span style="white-space: nowrap">
|
||||||
|
{{
|
||||||
|
studentList.length -
|
||||||
|
attendance.absent.length -
|
||||||
|
attendance.late.length -
|
||||||
|
attendance.exclude.length
|
||||||
|
}}人
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<h2>
|
||||||
|
<span style="white-space: nowrap"> 请假</span>
|
||||||
|
:
|
||||||
|
<span style="white-space: nowrap">
|
||||||
|
{{ attendance.absent.length }}人
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<h3
|
||||||
|
v-for="(name, index) in attendance.absent"
|
||||||
|
:key="'absent-' + index"
|
||||||
|
class="gray-text"
|
||||||
|
>
|
||||||
|
<span v-if="display.lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||||
|
><span style="white-space: nowrap">{{ name }}</span>
|
||||||
|
</h3>
|
||||||
|
<h2>
|
||||||
|
<span style="white-space: nowrap">迟到</span>
|
||||||
|
:
|
||||||
|
<span style="white-space: nowrap">
|
||||||
|
{{ attendance.late.length }}人
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<h3
|
||||||
|
v-for="(name, index) in attendance.late"
|
||||||
|
:key="'late-' + index"
|
||||||
|
class="gray-text"
|
||||||
|
>
|
||||||
|
<span v-if="display.lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||||
|
><span style="white-space: nowrap">{{ name }}</span>
|
||||||
|
</h3>
|
||||||
|
<h2>
|
||||||
|
<span style="white-space: nowrap">不参与</span>
|
||||||
|
:
|
||||||
|
<span style="white-space: nowrap">
|
||||||
|
{{ attendance.exclude.length }}人
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<h3
|
||||||
|
v-for="(name, index) in attendance.exclude"
|
||||||
|
:key="'exclude-' + index"
|
||||||
|
class="gray-text"
|
||||||
|
>
|
||||||
|
<span v-if="display.lgAndUp.value">{{ `${index + 1}. ` }}</span
|
||||||
|
><span style="white-space: nowrap">{{ name }}</span>
|
||||||
|
</h3>
|
||||||
|
</v-col>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useDisplay } from "vuetify";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AttendanceSidebar",
|
||||||
|
props: {
|
||||||
|
studentList: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
attendance: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["click"],
|
||||||
|
setup() {
|
||||||
|
const display = useDisplay();
|
||||||
|
return { display };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Add any specific styles here if needed, or rely on global styles */
|
||||||
|
</style>
|
||||||
118
src/components/home/HomeActions.vue
Normal file
118
src/components/home/HomeActions.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex flex-wrap align-center mt-4">
|
||||||
|
<v-btn
|
||||||
|
v-if="!synced"
|
||||||
|
:loading="loadingUpload"
|
||||||
|
class="ml-2"
|
||||||
|
color="error"
|
||||||
|
size="large"
|
||||||
|
@click="$emit('upload')"
|
||||||
|
>
|
||||||
|
上传
|
||||||
|
</v-btn>
|
||||||
|
<v-btn v-else color="success" size="large" @click="$emit('show-sync-message')">
|
||||||
|
同步完成
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="showRandomPickerButton"
|
||||||
|
append-icon="mdi-dice-multiple"
|
||||||
|
class="ml-2"
|
||||||
|
color="amber"
|
||||||
|
prepend-icon="mdi-account-question"
|
||||||
|
size="large"
|
||||||
|
@click="$emit('open-random-picker')"
|
||||||
|
>
|
||||||
|
随机点名
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="showExamScheduleButton"
|
||||||
|
class="ml-2"
|
||||||
|
color="green"
|
||||||
|
prepend-icon="mdi-calendar-check"
|
||||||
|
size="large"
|
||||||
|
@click="$router.push('/examschedule')"
|
||||||
|
>
|
||||||
|
考试看板
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="showListCardButton"
|
||||||
|
class="ml-2"
|
||||||
|
color="primary-darken-1"
|
||||||
|
prepend-icon="mdi-list-box"
|
||||||
|
size="large"
|
||||||
|
@click="$router.push('/list')"
|
||||||
|
>
|
||||||
|
列表
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="showFullscreenButton"
|
||||||
|
:color="isFullscreen ? 'blue-grey' : 'blue'"
|
||||||
|
:prepend-icon="
|
||||||
|
isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'
|
||||||
|
"
|
||||||
|
class="ml-2"
|
||||||
|
size="large"
|
||||||
|
@click="$emit('toggle-fullscreen')"
|
||||||
|
>
|
||||||
|
{{ isFullscreen ? "退出全屏" : "全屏显示" }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
v-if="showTestCardButton"
|
||||||
|
class="ml-2"
|
||||||
|
color="purple"
|
||||||
|
prepend-icon="mdi-test-tube"
|
||||||
|
size="large"
|
||||||
|
@click="$emit('add-test-card')"
|
||||||
|
>
|
||||||
|
添加测试卡片
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-card
|
||||||
|
v-if="showAntiScreenBurnCard"
|
||||||
|
border
|
||||||
|
class="mt-4 anti-burn-card"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
<v-card-title class="text-subtitle-1">
|
||||||
|
<v-icon icon="mdi-shield-check" size="small" start />
|
||||||
|
屏幕保护技术已启用
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="text-body-2">
|
||||||
|
<p>
|
||||||
|
为防止OLED/LCD屏幕烧屏,界面元素会定期微调位置。
|
||||||
|
</p>
|
||||||
|
<p class="text-caption text-grey">
|
||||||
|
此功能不会影响正常使用,仅在长时间静止显示时生效。
|
||||||
|
</p>
|
||||||
|
<p class="text-caption text-grey">
|
||||||
|
建议在放学后关闭显示器以节约能源。
|
||||||
|
</p>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "HomeActions",
|
||||||
|
props: {
|
||||||
|
synced: Boolean,
|
||||||
|
loadingUpload: Boolean,
|
||||||
|
showRandomPickerButton: Boolean,
|
||||||
|
showExamScheduleButton: Boolean,
|
||||||
|
showListCardButton: Boolean,
|
||||||
|
showFullscreenButton: Boolean,
|
||||||
|
isFullscreen: Boolean,
|
||||||
|
showAntiScreenBurnCard: Boolean,
|
||||||
|
showTestCardButton: Boolean,
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
"upload",
|
||||||
|
"show-sync-message",
|
||||||
|
"open-random-picker",
|
||||||
|
"toggle-fullscreen",
|
||||||
|
"add-test-card",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
222
src/components/home/HomeworkGrid.vue
Normal file
222
src/components/home/HomeworkGrid.vue
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="gridContainer" class="grid-masonry">
|
||||||
|
<TransitionGroup name="grid">
|
||||||
|
<div
|
||||||
|
v-for="item in sortedItems"
|
||||||
|
:key="item.key"
|
||||||
|
:style="{
|
||||||
|
'grid-row-end': `span ${item.rowSpan}`,
|
||||||
|
order: item.order,
|
||||||
|
}"
|
||||||
|
class="grid-item"
|
||||||
|
>
|
||||||
|
<!-- 出勤卡片 -->
|
||||||
|
<v-card
|
||||||
|
v-if="item.type === 'attendance'"
|
||||||
|
:class="{ 'glow-highlight': highlightedCards[item.key] }"
|
||||||
|
border
|
||||||
|
class="glow-track"
|
||||||
|
height="100%"
|
||||||
|
@click="$emit('open-attendance')"
|
||||||
|
@mousemove="handleMouseMove"
|
||||||
|
@touchmove="handleTouchMove"
|
||||||
|
>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2" color="primary" icon="mdi-account-group" />
|
||||||
|
出勤统计
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-2">
|
||||||
|
<span>应到/实到</span>
|
||||||
|
<span class="text-h6">
|
||||||
|
{{ item.data.total - item.data.exclude.length }}/{{
|
||||||
|
item.data.total -
|
||||||
|
item.data.absent.length -
|
||||||
|
item.data.late.length -
|
||||||
|
item.data.exclude.length
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<v-divider class="mb-2" />
|
||||||
|
|
||||||
|
<div v-if="item.data.absent.length > 0" class="mb-2">
|
||||||
|
<div class="text-error text-caption mb-1">请假 ({{ item.data.absent.length }})</div>
|
||||||
|
<div class="d-flex flex-wrap" style="gap: 4px">
|
||||||
|
<v-chip v-for="name in item.data.absent" :key="name" color="error" size="x-small" variant="flat">
|
||||||
|
{{ name }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.data.late.length > 0" class="mb-2">
|
||||||
|
<div class="text-warning text-caption mb-1">迟到 ({{ item.data.late.length }})</div>
|
||||||
|
<div class="d-flex flex-wrap" style="gap: 4px">
|
||||||
|
<v-chip v-for="name in item.data.late" :key="name" color="warning" size="x-small" variant="flat">
|
||||||
|
{{ name }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="item.data.exclude.length > 0" class="mb-2">
|
||||||
|
<div class="text-grey text-caption mb-1">不参与 ({{ item.data.exclude.length }})</div>
|
||||||
|
<div class="d-flex flex-wrap" style="gap: 4px">
|
||||||
|
<v-chip v-for="name in item.data.exclude" :key="name" color="grey" size="x-small" variant="flat">
|
||||||
|
{{ name }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
item.data.absent.length === 0 &&
|
||||||
|
item.data.late.length === 0 &&
|
||||||
|
item.data.exclude.length === 0
|
||||||
|
"
|
||||||
|
class="text-success text-center mt-2"
|
||||||
|
>
|
||||||
|
全勤
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 自定义/测试卡片 -->
|
||||||
|
<v-card
|
||||||
|
v-else-if="item.type === 'custom'"
|
||||||
|
:class="{ 'glow-highlight': highlightedCards[item.key] }"
|
||||||
|
border
|
||||||
|
class="glow-track"
|
||||||
|
height="100%"
|
||||||
|
@click="!isEditingDisabled && $emit('open-dialog', item.key)"
|
||||||
|
@mousemove="handleMouseMove"
|
||||||
|
@touchmove="handleTouchMove"
|
||||||
|
>
|
||||||
|
<v-card-title class="text-primary">
|
||||||
|
<v-icon class="mr-2" icon="mdi-card-text-outline" size="small" />
|
||||||
|
{{ item.name }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text :style="contentStyle">
|
||||||
|
{{ item.content }}
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
|
||||||
|
<!-- 普通作业卡片 -->
|
||||||
|
<v-card
|
||||||
|
v-else
|
||||||
|
:class="{ 'glow-highlight': highlightedCards[item.key] }"
|
||||||
|
border
|
||||||
|
class="glow-track"
|
||||||
|
height="100%"
|
||||||
|
@click="!isEditingDisabled && $emit('open-dialog', item.key)"
|
||||||
|
@mousemove="handleMouseMove"
|
||||||
|
@touchmove="handleTouchMove"
|
||||||
|
>
|
||||||
|
<v-card-title>{{ item.name }}</v-card-title>
|
||||||
|
<v-card-text :style="contentStyle">
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
v-for="text in splitPoint(item.content)"
|
||||||
|
:key="text"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 单独显示空科目 -->
|
||||||
|
<div class="empty-subjects mt-4">
|
||||||
|
<template v-if="emptySubjectDisplay === 'button'">
|
||||||
|
<v-btn-group divided variant="tonal">
|
||||||
|
<v-btn
|
||||||
|
v-for="subject in unusedSubjects"
|
||||||
|
:key="subject.name"
|
||||||
|
:disabled="isEditingDisabled"
|
||||||
|
@click="$emit('open-dialog', subject.name)"
|
||||||
|
>
|
||||||
|
<v-icon start> mdi-plus</v-icon>
|
||||||
|
{{ subject.name }}
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-group>
|
||||||
|
</template>
|
||||||
|
<div v-else class="empty-subjects-grid">
|
||||||
|
<TransitionGroup name="v-list">
|
||||||
|
<v-card
|
||||||
|
v-for="subject in unusedSubjects"
|
||||||
|
:key="subject.name"
|
||||||
|
:disabled="isEditingDisabled"
|
||||||
|
border
|
||||||
|
class="empty-subject-card"
|
||||||
|
@click="$emit('open-dialog', subject.name)"
|
||||||
|
>
|
||||||
|
<v-card-title class="text-subtitle-1">
|
||||||
|
{{ subject.name }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
<v-icon color="grey" size="small"> mdi-plus</v-icon>
|
||||||
|
<div class="text-caption text-grey">点击添加作业</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "HomeworkGrid",
|
||||||
|
props: {
|
||||||
|
sortedItems: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
unusedSubjects: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
emptySubjectDisplay: {
|
||||||
|
type: String,
|
||||||
|
default: "button",
|
||||||
|
},
|
||||||
|
isEditingDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
highlightedCards: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["open-dialog", "open-attendance"],
|
||||||
|
methods: {
|
||||||
|
splitPoint(content) {
|
||||||
|
return content.split("\n").filter((text) => text.trim());
|
||||||
|
},
|
||||||
|
handleMouseMove(e) {
|
||||||
|
const card = e.currentTarget;
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
|
const y = ((e.clientY - rect.top) / rect.height) * 100;
|
||||||
|
card.style.setProperty("--x", `${x}%`);
|
||||||
|
card.style.setProperty("--y", `${y}%`);
|
||||||
|
},
|
||||||
|
handleTouchMove(e) {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const card = e.currentTarget;
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const x = ((touch.clientX - rect.left) / rect.width) * 100;
|
||||||
|
const y = ((touch.clientY - rect.top) / rect.height) * 100;
|
||||||
|
card.style.setProperty("--x", `${x}%`);
|
||||||
|
card.style.setProperty("--y", `${y}%`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
File diff suppressed because it is too large
Load Diff
28
src/styles/glow.scss
Normal file
28
src/styles/glow.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.glow-effect {
|
||||||
|
transition: box-shadow 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 15px rgba(var(--v-theme-primary), 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-text {
|
||||||
|
text-shadow: 0 0 5px rgba(var(--v-theme-primary), 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bloom-container {
|
||||||
|
.v-card {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 0 15px rgba(var(--v-theme-primary), 0.3) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 10px rgba(var(--v-theme-primary), 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
@import './glow.scss';
|
||||||
|
|
||||||
// 添加卡片发光效果
|
// 添加卡片发光效果
|
||||||
.glow-track {
|
.glow-track {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user