1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-05 02:59:23 +00:00
This commit is contained in:
SunWuyuan 2025-03-02 12:03:25 +08:00
parent e31578ee10
commit 367954cfa6
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
4 changed files with 374 additions and 38 deletions

View File

@ -14,6 +14,8 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"roboto-fontface": "*", "roboto-fontface": "*",
"vue": "^3.4.31", "vue": "^3.4.31",
"vue-masonry-wall": "^0.3.2",
"vue-waterfall-plugin-next": "^2.6.5",
"vuetify": "^3.6.14" "vuetify": "^3.6.14"
}, },
"devDependencies": { "devDependencies": {

42
pnpm-lock.yaml generated
View File

@ -20,6 +20,12 @@ importers:
vue: vue:
specifier: ^3.4.31 specifier: ^3.4.31
version: 3.5.13 version: 3.5.13
vue-masonry-wall:
specifier: ^0.3.2
version: 0.3.2(lodash@4.17.21)(vue@3.5.13)
vue-waterfall-plugin-next:
specifier: ^2.6.5
version: 2.6.5
vuetify: vuetify:
specifier: ^3.6.14 specifier: ^3.6.14
version: 3.7.4(vite-plugin-vuetify@2.0.4)(vue@3.5.13) version: 3.7.4(vite-plugin-vuetify@2.0.4)(vue@3.5.13)
@ -360,55 +366,46 @@ packages:
resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==} resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.27.2': '@rollup/rollup-linux-arm-musleabihf@4.27.2':
resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==} resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.27.2': '@rollup/rollup-linux-arm64-gnu@4.27.2':
resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==} resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.27.2': '@rollup/rollup-linux-arm64-musl@4.27.2':
resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==} resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.27.2': '@rollup/rollup-linux-powerpc64le-gnu@4.27.2':
resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==} resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.27.2': '@rollup/rollup-linux-riscv64-gnu@4.27.2':
resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==} resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.27.2': '@rollup/rollup-linux-s390x-gnu@4.27.2':
resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==} resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.27.2': '@rollup/rollup-linux-x64-gnu@4.27.2':
resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==} resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.27.2': '@rollup/rollup-linux-x64-musl@4.27.2':
resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==} resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.27.2': '@rollup/rollup-win32-arm64-msvc@4.27.2':
resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==} resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==}
@ -1727,11 +1724,24 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=6.0.0' eslint: '>=6.0.0'
vue-masonry-wall@0.3.2:
resolution: {integrity: sha512-uy/tY9Lz6zVZCXmS78sv5u1yf70gAC+ElFXdV8miJfLiNnzXXt2i03I8sccx2YXDKk1IOZv6wDbKTUL8ethvfw==}
engines: {node: '>=10'}
peerDependencies:
lodash: ^4.17.15
vue: ^2.6.10
vue-observe-visibility@0.4.6:
resolution: {integrity: sha512-xo0CEVdkjSjhJoDdLSvoZoQrw/H2BlzB5jrCBKGZNXN2zdZgMuZ9BKrxXDjNP2AxlcCoKc8OahI3F3r3JGLv2Q==}
vue-router@4.4.5: vue-router@4.4.5:
resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==} resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==}
peerDependencies: peerDependencies:
vue: ^3.2.0 vue: ^3.2.0
vue-waterfall-plugin-next@2.6.5:
resolution: {integrity: sha512-8ACGbdjoyKLiJfnKXB8h8f9eE14lhyzfI1N1nrfVAIRczSpNY1KRwGOnVXN5OHqheLl3V1C0uVVRPtjTJkHkhw==}
vue@3.5.13: vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
peerDependencies: peerDependencies:
@ -2088,7 +2098,7 @@ snapshots:
'@vue/shared@3.5.13': {} '@vue/shared@3.5.13': {}
'@vuetify/loader-shared@2.0.3(vue@3.5.13)(vuetify@3.7.4(vite-plugin-vuetify@2.0.4)(vue@3.5.13))': '@vuetify/loader-shared@2.0.3(vue@3.5.13)(vuetify@3.7.4)':
dependencies: dependencies:
upath: 2.0.1 upath: 2.0.1
vue: 3.5.13 vue: 3.5.13
@ -3443,7 +3453,7 @@ snapshots:
vite-plugin-vuetify@2.0.4(vite@5.4.11(sass-embedded@1.81.0)(sass@1.77.8))(vue@3.5.13)(vuetify@3.7.4): vite-plugin-vuetify@2.0.4(vite@5.4.11(sass-embedded@1.81.0)(sass@1.77.8))(vue@3.5.13)(vuetify@3.7.4):
dependencies: dependencies:
'@vuetify/loader-shared': 2.0.3(vue@3.5.13)(vuetify@3.7.4(vite-plugin-vuetify@2.0.4)(vue@3.5.13)) '@vuetify/loader-shared': 2.0.3(vue@3.5.13)(vuetify@3.7.4)
debug: 4.3.7 debug: 4.3.7
upath: 2.0.1 upath: 2.0.1
vite: 5.4.11(sass-embedded@1.81.0)(sass@1.77.8) vite: 5.4.11(sass-embedded@1.81.0)(sass@1.77.8)
@ -3479,11 +3489,21 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
vue-masonry-wall@0.3.2(lodash@4.17.21)(vue@3.5.13):
dependencies:
lodash: 4.17.21
vue: 3.5.13
vue-observe-visibility: 0.4.6
vue-observe-visibility@0.4.6: {}
vue-router@4.4.5(vue@3.5.13): vue-router@4.4.5(vue@3.5.13):
dependencies: dependencies:
'@vue/devtools-api': 6.6.4 '@vue/devtools-api': 6.6.4
vue: 3.5.13 vue: 3.5.13
vue-waterfall-plugin-next@2.6.5: {}
vue@3.5.13: vue@3.5.13:
dependencies: dependencies:
'@vue/compiler-dom': 3.5.13 '@vue/compiler-dom': 3.5.13

View File

@ -58,46 +58,68 @@
fluid fluid
> >
<v-row> <v-row>
<v-col cols="11"> <v-col :cols="attendanceVisible ? 11 : 12">
<v-container <div class="grid-masonry" ref="gridContainer">
fluid <div
style="padding-left: 2px; padding-right: 2px" 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-row <v-card border height="100%">
v-for="subjects in homeworkArrange" <v-card-title :class="{ 'text-subtitle-1': !item.content }">
:key="subjects.name" {{ item.name }}
> </v-card-title>
<v-col <v-card-text :style="item.content ? contentStyle : null">
v-for="subject in subjects" <template v-if="item.content">
:key="subject"
cols="4"
style="padding: 2px !important"
@click="openDialog(subject)"
>
<v-card border>
<v-card-title>{{ homeworkData[subject].name }}</v-card-title>
<v-card-text :style="contentStyle">
<v-list> <v-list>
<v-list-item <v-list-item
v-for="text in splitPoint(homeworkData[subject].content)" v-for="text in splitPoint(item.content)"
:key="text" :key="text"
> >
{{ text }} {{ text }}
</v-list-item> </v-list-item>
</v-list> </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-text>
</v-card> </v-card>
</v-col> </div>
</v-row> </div>
</v-container>
<div />
</v-col>
<!-- 空科目按钮组 -->
<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="openDialog(subject.key)"
>
<v-icon start>mdi-plus</v-icon>
{{ subject.name }}
</v-btn>
</div>
</v-col>
<v-col <v-col
v-if="studentList.length" v-if="studentList.length"
class="attendance-area" class="attendance-area"
cols="1" :cols="1"
@click="setAttendanceArea" @click="setAttendanceArea"
> >
<h1>出勤</h1> <h1>出勤</h1>
@ -120,6 +142,22 @@
> >
{{ `${index + 1}. ${studentList[i]}` }} {{ `${index + 1}. ${studentList[i]}` }}
</h3> </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)"
>
{{ subject.name }}
</v-btn>
</template>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
@ -213,6 +251,88 @@
</v-snackbar> </v-snackbar>
</template> </template>
<style scoped>
.grid-masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 8px;
grid-auto-flow: dense;
}
.grid-item {
width: 100%;
transition: all 0.2s ease;
}
.empty-card {
transform: scale(0.9);
opacity: 0.8;
grid-row-end: span 1 !important;
}
.empty-card:hover {
transform: scale(0.95);
opacity: 1;
}
.empty-subjects-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.empty-subject-btn {
flex: 1;
min-width: 120px;
}
@media (max-width: 1199px) {
.grid-masonry {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 799px) {
.grid-masonry {
grid-template-columns: 1fr;
}
.empty-subject-btn {
min-width: 100px;
}
.empty-card {
transform: scale(0.95);
}
}
/* 确保容器高度不超过视口 */
.main-window {
max-height: calc(100vh - 180px);
overflow-y: auto;
}
/* 优化滚动条样式 */
.main-window::-webkit-scrollbar {
width: 8px;
}
.main-window::-webkit-scrollbar-track {
background: transparent;
}
.main-window::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.main-window::-webkit-scrollbar-thumb:hover {
background-color: rgba(0, 0, 0, 0.3);
}
</style>
<script> <script>
import axios from "axios"; import axios from "axios";
import { useDisplay } from "vuetify"; import { useDisplay } from "vuetify";
@ -246,6 +366,12 @@ export default {
refreshInterval: null, refreshInterval: null,
autoSave: false, autoSave: false,
refreshBeforeEdit: false, refreshBeforeEdit: false,
showEmptySubjects: localStorage.getItem('showEmptySubjects') === 'true',
emptySubjectDisplay: localStorage.getItem('emptySubjectDisplay') || 'card',
subjectOrder: [
"语文", "数学", "英语", "物理", "化学",
"生物", "政治", "历史", "地理", "其他"
],
}; };
}, },
@ -272,6 +398,43 @@ export default {
return `${this.dateString}的作业`; return `${this.dateString}的作业`;
} }
}, },
sortedItems() {
const items = Object.entries(this.homeworkData)
.map(([key, value]) => ({
key,
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
}))
.filter(item => {
if (this.emptySubjectDisplay === 'button') {
return item.content;
}
return true;
});
//
return this.optimizeGridLayout(items);
},
attendanceVisible() {
return this.studentList.length > 0;
},
emptySubjects() {
if (this.emptySubjectDisplay !== 'button') return [];
return Object.entries(this.homeworkData)
.map(([key, value]) => ({
key,
name: value.name,
content: value.content,
order: this.subjectOrder.indexOf(key)
}))
.filter(subject => !subject.content)
.sort((a, b) => a.order - b.order);
},
}, },
async mounted() { async mounted() {
@ -563,6 +726,65 @@ export default {
clearInterval(this.refreshInterval); clearInterval(this.refreshInterval);
} }
}, },
optimizeGridLayout(items) {
//
const sortedItems = items.sort((a, b) => {
//
if (a.content && !b.content) return -1;
if (!a.content && b.content) return 1;
//
if (a.content && b.content) {
const lengthDiff = b.rowSpan - a.rowSpan;
if (lengthDiff !== 0) return lengthDiff;
}
//
return a.order - b.order;
});
//
const columnHeights = [0, 0, 0];
const columnItems = [[], [], []];
//
sortedItems.forEach(item => {
const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
columnHeights[shortestColumn] += item.rowSpan;
columnItems[shortestColumn].push(item);
});
//
return columnItems.flat().map((item, index) => ({
...item,
order: index
}));
}
}, },
watch: {
homeworkData: {
handler() {
this.$nextTick(() => {
if (this.$refs.waterfall) {
this.$refs.waterfall.reflow();
}
});
},
deep: true
},
//
'$vuetify.display.width': {
handler() {
this.$nextTick(() => {
if (this.$refs.gridContainer) {
//
this.optimizeGridLayout(this.sortedItems);
}
});
}
}
}
}; };
</script> </script>

View File

@ -484,6 +484,86 @@
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<v-switch
v-model="showEmptySubjects"
label="显示空作业科目"
hint="是否在主界面显示没有作业内容的科目"
persistent-hint
@change="saveSettings"
/>
<v-col cols="12" md="6">
<v-card elevation="2" class="rounded-lg">
<v-card-item>
<template v-slot:prepend>
<v-icon icon="mdi-card-outline" size="large" class="mr-2" />
</template>
<v-card-title class="text-h6">空作业显示设置</v-card-title>
</v-card-item>
<v-card-text>
<v-radio-group
v-model="emptySubjectDisplay"
label="空作业显示方式"
>
<v-radio
value="card"
label="显示为空卡片"
>
<template v-slot:label>
<div class="d-flex align-center">
显示为空卡片
<v-tooltip location="right">
<template v-slot:activator="{ props }">
<v-icon
v-bind="props"
icon="mdi-help-circle-outline"
size="small"
class="ml-2"
/>
</template>
在主界面中显示为可点击的空白卡片
</v-tooltip>
</div>
</template>
</v-radio>
<v-radio
value="button"
label="显示为按钮组"
>
<template v-slot:label>
<div class="d-flex align-center">
显示为按钮组
<v-tooltip location="right">
<template v-slot:activator="{ props }">
<v-icon
v-bind="props"
icon="mdi-help-circle-outline"
size="small"
class="ml-2"
/>
</template>
在主界面底部显示为一组添加按钮
</v-tooltip>
</div>
</template>
</v-radio>
</v-radio-group>
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-btn
color="primary"
prepend-icon="mdi-content-save"
block
@click="saveEmptySubjectSettings"
>
保存设置
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-container> </v-container>
<v-snackbar v-model="snackbar"> <v-snackbar v-model="snackbar">
@ -575,6 +655,8 @@ export default {
studentToMove: null, studentToMove: null,
touchStartTime: 0, touchStartTime: 0,
touchTimeout: null, touchTimeout: null,
showEmptySubjects: localStorage.getItem('showEmptySubjects') === 'true',
emptySubjectDisplay: localStorage.getItem('emptySubjectDisplay') || 'card',
}; };
}, },
@ -824,6 +906,16 @@ export default {
} }
} }
}, },
saveSettings() {
localStorage.setItem('showEmptySubjects', this.showEmptySubjects.toString());
localStorage.setItem('emptySubjectDisplay', this.emptySubjectDisplay);
},
saveEmptySubjectSettings() {
this.saveSettings();
this.showMessage('保存成功');
}
}, },
}; };
</script> </script>