This commit is contained in:
SunWuyuan
2025-03-02 14:03:30 +08:00
parent 367954cfa6
commit c6b68ed3a0
3 changed files with 965 additions and 399 deletions
+391 -172
View File
@@ -53,114 +53,199 @@
/>
</template>
</v-app-bar>
<v-container
class="main-window"
fluid
>
<v-row>
<v-col :cols="attendanceVisible ? 11 : 12">
<div class="grid-masonry" ref="gridContainer">
<div
v-for="item in sortedItems"
:key="item.key"
class="grid-item"
:class="{
'empty-card': !item.content,
[`grid-row-${item.rowSpan}`]: true
}"
:style="{
'grid-row-end': `span ${item.rowSpan}`,
order: item.order
}"
@click="openDialog(item.key)"
>
<v-card border height="100%">
<v-card-title :class="{ 'text-subtitle-1': !item.content }">
{{ item.name }}
<div class="d-flex">
<!-- 主要内容区域 -->
<v-container
class="main-window flex-grow-1"
fluid
>
<div v-if="showNoDataMessage && !isToday" class="no-data-message">
<v-card class="text-center pa-4" variant="outlined">
<v-card-title class="text-h6">{{ noDataMessage }}</v-card-title>
<v-card-text>
<div class="text-body-1">该日期未上传作业数据</div>
</v-card-text>
</v-card>
</div>
<template v-else>
<div v-if="showNoDataMessage && isToday" class="no-data-message">
<v-card class="text-center pa-4" variant="outlined">
<v-card-title class="text-h6">开始今天的作业</v-card-title>
<v-card-text>
<div class="text-body-1 mb-4">今天还没有上传作业哦~</div>
<v-btn
color="primary"
@click="initializeAndOpenDialog"
>
开始添加作业
</v-btn>
</v-card-text>
</v-card>
</div>
<template v-else>
<div class="grid-masonry" ref="gridContainer">
<div
v-for="item in sortedItems"
:key="item.key"
class="grid-item"
:class="{
'empty-card': !item.content,
[`grid-row-${item.rowSpan}`]: true
}"
:style="{
'grid-row-end': `span ${item.rowSpan}`,
order: item.order,
cursor: uploadLoading || downloadLoading ? 'not-allowed' : 'pointer',
opacity: uploadLoading || downloadLoading ? '0.7' : '1'
}"
@click="!uploadLoading && !downloadLoading && openDialog(item.key)"
>
<v-card border height="100%">
<v-card-title :class="{ 'text-subtitle-1': !item.content }">
{{ item.name }}
</v-card-title>
<v-card-text :style="item.content ? contentStyle : null">
<template v-if="item.content">
<v-list>
<v-list-item
v-for="text in splitPoint(item.content)"
:key="text"
>
{{ text }}
</v-list-item>
</v-list>
</template>
<template v-else>
<div class="text-center pa-2">
<v-icon size="small" color="grey">mdi-plus</v-icon>
<div class="text-caption text-grey">点击添加作业</div>
</div>
</template>
</v-card-text>
</v-card>
</div>
</div>
<!-- 空科目按钮组 -->
<div v-if="emptySubjectDisplay === 'button'" class="empty-subjects-container">
<v-btn
v-for="subject in emptySubjects"
:key="subject.key"
variant="outlined"
color="primary"
class="empty-subject-btn"
@click="!uploadLoading && !downloadLoading && openDialog(subject.key)"
:disabled="uploadLoading || downloadLoading"
>
<v-icon start>mdi-plus</v-icon>
{{ subject.name }}
</v-btn>
</div>
</template>
</template>
</v-container>
<!-- 出勤统计区域 -->
<v-navigation-drawer
v-if="studentList.length > 1"
location="right"
permanent
width="300"
class="attendance-drawer"
>
<v-card
class="h-100"
flat
>
<v-card-item>
<v-card-title class="text-h6">
<v-icon icon="mdi-account-multiple" class="mr-2" />
出勤统计
</v-card-title>
</v-card-item>
<v-card-text>
<div class="attendance-stats mb-4">
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-subtitle-1">应到人数</span>
<span class="text-h6">{{ studentList.length }}人</span>
</div>
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-subtitle-1">实到人数</span>
<span class="text-h6 text-success">
{{ studentList.length - selectedSet.size - lateSet.size }}人
</span>
</div>
<v-divider class="my-2" />
<div class="d-flex justify-space-between align-center mb-2">
<span class="text-subtitle-1">请假人数</span>
<span class="text-h6 text-error">{{ selectedSet.size }}人</span>
</div>
<div class="d-flex justify-space-between align-center">
<span class="text-subtitle-1">迟到人数</span>
<span class="text-h6 text-warning">{{ lateSet.size }}人</span>
</div>
</div>
<template v-if="selectedSet.size > 0">
<v-card
variant="outlined"
class="mb-4"
>
<v-card-title class="text-subtitle-1">
<v-icon icon="mdi-account-off" class="mr-2" color="error" />
请假名单
</v-card-title>
<v-card-text :style="item.content ? contentStyle : null">
<template v-if="item.content">
<v-list>
<v-list-item
v-for="text in splitPoint(item.content)"
:key="text"
>
{{ text }}
</v-list-item>
</v-list>
</template>
<template v-else>
<div class="text-center pa-2">
<v-icon size="small" color="grey">mdi-plus</v-icon>
<div class="text-caption text-grey">点击添加作业</div>
</div>
</template>
<v-card-text>
<v-list density="compact" nav>
<v-list-item
v-for="i in Array.from(selectedSet)"
:key="'absent-' + i"
:title="`${i + 1}. ${studentList[i]}`"
prepend-icon="mdi-account"
/>
</v-list>
</v-card-text>
</v-card>
</div>
</div>
</template>
<!-- 空科目按钮组 -->
<div v-if="emptySubjectDisplay === 'button'" class="empty-subjects-container">
<template v-if="lateSet.size > 0">
<v-card
variant="outlined"
>
<v-card-title class="text-subtitle-1">
<v-icon icon="mdi-clock-alert" class="mr-2" color="warning" />
迟到名单
</v-card-title>
<v-card-text>
<v-list density="compact" nav>
<v-list-item
v-for="i in Array.from(lateSet)"
:key="'late-' + i"
:title="`${i + 1}. ${studentList[i]}`"
prepend-icon="mdi-account-clock"
/>
</v-list>
</v-card-text>
</v-card>
</template>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn
v-for="subject in emptySubjects"
:key="subject.key"
variant="outlined"
color="primary"
class="empty-subject-btn"
@click="openDialog(subject.key)"
>
<v-icon start>mdi-plus</v-icon>
{{ subject.name }}
</v-btn>
</div>
</v-col>
<v-col
v-if="studentList.length"
class="attendance-area"
:cols="1"
@click="setAttendanceArea"
>
<h1>出勤</h1>
<h2>应到:{{ studentList.length }}人</h2>
<h2>
实到:{{ studentList.length - selectedSet.size }}人
</h2>
<h2>请假:{{ selectedSet.size }} 人</h2>
<h3
v-for="(i, index) in selectedSet"
:key="'absent-' + index"
>
{{ `${index + 1}. ${studentList[i]}` }}
</h3>
<h2>迟到:{{ lateSet.size }} 人</h2>
<h3
v-for="(i, index) in lateSet"
:key="'late-' + index"
>
{{ `${index + 1}. ${studentList[i]}` }}
</h3>
<!-- 空科目按钮显示区域 -->
<template v-if="showEmptySubjects && emptySubjectDisplay === 'button'">
<v-divider class="my-4" />
<h2>未填写作业</h2>
<v-btn
v-for="subject in emptySubjects"
:key="subject.key"
block
variant="outlined"
class="mb-2"
@click.stop="openDialog(subject.key)"
color="primary"
prepend-icon="mdi-pencil"
@click="setAttendanceArea"
>
{{ subject.name }}
编辑出勤状态
</v-btn>
</template>
</v-col>
</v-row>
</v-container>
</v-card-actions>
</v-card>
</v-navigation-drawer>
</div>
<v-container fluid>
<v-btn
v-if="!synced"
@@ -331,11 +416,65 @@
.main-window::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.3);
}
.no-data-message {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
margin: 20px 0;
}
.attendance-drawer {
border-left: 1px solid rgba(0, 0, 0, 0.12);
}
.attendance-drawer :deep(.v-navigation-drawer__content) {
overflow-y: auto;
}
/* 优化滚动条样式 */
.attendance-drawer :deep(.v-navigation-drawer__content::-webkit-scrollbar) {
width: 8px;
}
.attendance-drawer :deep(.v-navigation-drawer__content::-webkit-scrollbar-track) {
background: transparent;
}
.attendance-drawer :deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb) {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.attendance-drawer :deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb:hover) {
background-color: rgba(0, 0, 0, 0.3);
}
/* 响应式调整 */
@media (max-width: 960px) {
.attendance-drawer {
display: none;
}
}
.text-success {
color: rgb(var(--v-theme-success));
}
.text-error {
color: rgb(var(--v-theme-error));
}
.text-warning {
color: rgb(var(--v-theme-warning));
}
</style>
<script>
import axios from "axios";
import { useDisplay } from "vuetify";
import { getSetting, watchSettings } from '@/utils/settings';
export default {
name: "HomeworkBoard",
@@ -353,25 +492,24 @@ export default {
dateString: "",
synced: false,
attendDialogVisible: false,
contentStyle: { "font-size": "28px" },
contentStyle: { "font-size": `${getSetting('font.size')}px` },
uploadLoading: false,
downloadLoading: false,
homeworkData: {},
homeworkArrange: [[], []],
snackbar: false,
snackbarText: "",
fontSize: parseInt(localStorage.getItem('fontSize')) || 28,
fontSize: getSetting('font.size'),
datePickerDialog: false,
selectedDate: null,
refreshInterval: null,
autoSave: false,
refreshBeforeEdit: false,
showEmptySubjects: localStorage.getItem('showEmptySubjects') === 'true',
emptySubjectDisplay: localStorage.getItem('emptySubjectDisplay') || 'card',
subjectOrder: [
"语文", "数学", "英语", "物理", "化学",
"生物", "政治", "历史", "地理", "其他"
],
showNoDataMessage: false,
noDataMessage: '',
isToday: false,
};
},
@@ -405,7 +543,6 @@ export default {
name: value.name,
content: value.content,
order: this.subjectOrder.indexOf(key),
// 计算每个卡片的行数
rowSpan: value.content ?
Math.ceil((value.content.split('\n').filter(line => line.trim()).length + 1) * 0.8) : 1
}))
@@ -416,8 +553,12 @@ export default {
return true;
});
// 对项目进行排序和优化布局
return this.optimizeGridLayout(items);
// 根据排序模式选择不同的排序方法
if (this.dynamicSort) {
return this.optimizeGridLayout(items);
} else {
return this.fixedGridLayout(items);
}
},
attendanceVisible() {
return this.studentList.length > 0;
@@ -435,6 +576,18 @@ export default {
.filter(subject => !subject.content)
.sort((a, b) => a.order - b.order);
},
autoSave() {
return getSetting('edit.autoSave');
},
refreshBeforeEdit() {
return getSetting('edit.refreshBeforeEdit');
},
emptySubjectDisplay() {
return getSetting('display.emptySubjectDisplay');
},
dynamicSort() {
return getSetting('display.dynamicSort');
},
},
async mounted() {
@@ -442,14 +595,26 @@ export default {
this.updateBackendUrl();
await this.initializeData();
this.setupAutoRefresh();
this.autoSave = localStorage.getItem('autoSave') === 'true';
this.refreshBeforeEdit = localStorage.getItem('refreshBeforeEdit') === 'true';
// 监听设置变化
this.unwatchSettings = watchSettings(() => {
this.updateSettings();
});
} catch (err) {
console.error("初始化失败:", err);
this.showError("初始化失败,请刷新页面重试");
}
},
beforeUnmount() {
if (this.unwatchSettings) {
this.unwatchSettings();
}
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
},
methods: {
async initializeData() {
const res = await axios.get(`${this.backurl}/config`);
@@ -593,7 +758,7 @@ export default {
this.contentStyle = {
"font-size": `${this.fontSize}px`,
};
localStorage.setItem('fontSize', this.fontSize.toString());
setSetting('font.size', this.fontSize);
},
async uploadData() {
@@ -630,64 +795,42 @@ export default {
},
async downloadDataDirectly() {
const formattedDate = new Date(this.dateString).toISOString().split('T')[0];
const res = await axios.get(
`${this.backurl}/homework?date=${formattedDate}`
);
this.homeworkData = res.data.data || this.homeworkData;
try {
const formattedDate = new Date(this.dateString).toISOString().split('T')[0];
const res = await axios.get(
`${this.backurl}/homework?date=${formattedDate}`
);
const subjectOrder = [
"语文",
"数学",
"英语",
"物理",
"化学",
"生物",
"政治",
"历史",
"地理",
"其他",
];
// 1. 将对象转换成数组,方便排序
let sortedSubjects = Object.keys(this.homeworkData).map((key) => ({
key,
...this.homeworkData[key],
}));
// 检查是否是今天的日期
const today = new Date();
const isToday = today.toISOString().split('T')[0] === formattedDate;
this.isToday = isToday;
// 2. 按照指定的顺序排序
sortedSubjects.sort((a, b) => {
const indexA = subjectOrder.indexOf(a.key);
const indexB = subjectOrder.indexOf(b.key);
// 优先根据学科顺序排序
if (indexA !== indexB) {
return indexA - indexB;
if (!res.data.status) {
this.showNoDataMessage = true;
this.noDataMessage = res.data.msg || '未找到数据';
// 不再初始化作业数据,保持为空
this.homeworkData = {};
this.selectedSet.clear();
this.lateSet.clear();
return;
}
return 0;
});
// 3. 进一步排序:将 content 为空的项移动到最后
sortedSubjects = [
// 先放置有 content 的项
...sortedSubjects.filter((item) => item.content != ""),
// 再放置 content 为空的项
...sortedSubjects.filter((item) => item.content == ""),
];
// 4. 将排序后的数组转换回对象
this.homeworkData = sortedSubjects.reduce((acc, curr) => {
acc[curr.key] = { name: curr.name, content: curr.content };
return acc;
}, {});
this.selectedSet = new Set(res.data.attendance || []);
this.lateSet = new Set(res.data.late || []); // Initialize late set
this.synced = true;
// 有数据的情况
this.showNoDataMessage = false;
this.homeworkData = res.data.data || {};
this.selectedSet = new Set(res.data.attendance || []);
this.lateSet = new Set(res.data.late || []);
this.synced = true;
} catch (error) {
console.error('下载数据失败:', error);
this.showError('下载数据失败,请重试');
}
},
updateBackendUrl() {
const domain = localStorage.getItem('backendServerDomain');
const classNum = localStorage.getItem('classNumber');
const domain = getSetting('server.domain');
const classNum = getSetting('server.classNumber');
if (domain && classNum) {
this.backurl = `${domain}/${classNum}`;
@@ -696,8 +839,12 @@ export default {
},
setupAutoRefresh() {
const autoRefresh = localStorage.getItem('autoRefresh') === 'true';
const interval = parseInt(localStorage.getItem('refreshInterval')) || 300;
const autoRefresh = getSetting('refresh.auto');
const interval = getSetting('refresh.interval');
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
if (autoRefresh) {
this.refreshInterval = setInterval(() => {
@@ -706,6 +853,18 @@ export default {
}
},
updateSettings() {
// 更新字体大小
this.fontSize = getSetting('font.size');
this.contentStyle = { "font-size": `${this.fontSize}px` };
// 更新自动刷新
this.setupAutoRefresh();
// 更新服务器设置
this.updateBackendUrl();
},
handleDateSelect(newDate) {
if (newDate) {
// 使用本地时区处理日期,避免时区偏移
@@ -721,12 +880,6 @@ export default {
}
},
beforeDestroy() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
},
optimizeGridLayout(items) {
// 首先按内容长度和科目顺序排序
const sortedItems = items.sort((a, b) => {
@@ -760,7 +913,73 @@ export default {
...item,
order: index
}));
}
},
fixedGridLayout(items) {
// 定义每行的科目顺序
const rowSubjects = [
["语文", "数学", "英语"],
["物理", "化学", "生物"],
["政治", "历史", "地理", "其他"]
];
// 按照固定布局排序
return items.sort((a, b) => {
// 找出科目所在的行号
const getRowIndex = (subject) => {
for (let i = 0; i < rowSubjects.length; i++) {
if (rowSubjects[i].includes(subject)) {
return i;
}
}
return rowSubjects.length; // 其他科目放最后
};
// 获取科目在行内的位置
const getColumnIndex = (subject) => {
for (const row of rowSubjects) {
const index = row.indexOf(subject);
if (index !== -1) return index;
}
return 999; // 未知科目放最后
};
const rowA = getRowIndex(a.key);
const rowB = getRowIndex(b.key);
if (rowA !== rowB) {
return rowA - rowB;
}
// 同一行内按照预定义顺序排序
const colA = getColumnIndex(a.key);
const colB = getColumnIndex(b.key);
return colA - colB;
}).map((item, index) => ({
...item,
order: index,
// 固定布局时每个卡片占用相同的行高
rowSpan: item.content ? 2 : 1
}));
},
initializeAndOpenDialog() {
// 初始化作业数据
this.initializeHomeworkData();
this.showNoDataMessage = false;
this.synced = false; // 设置为未同步状态
// 如果启用了自动保存,初始化后自动上传
if (this.autoSave) {
this.uploadData();
}
// 打开第一个科目的对话框
const firstSubject = Object.keys(this.homeworkData)[0];
if (firstSubject) {
this.openDialog(firstSubject);
}
},
},
watch: {