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:
Sunwuyuan
2025-11-29 13:59:32 +08:00
parent b3595422c7
commit 8d9b9a3f32
8 changed files with 1062 additions and 798 deletions
+118
View 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
View 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>