1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-05 11:09:29 +00:00
This commit is contained in:
SunWuyuan 2025-03-02 15:34:27 +08:00
parent 99f3f1b0ef
commit 5cfeba57ba
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
3 changed files with 283 additions and 597 deletions

View File

@ -24,7 +24,6 @@
<v-menu <v-menu
v-model="datePickerDialog" v-model="datePickerDialog"
:close-on-content-click="false" :close-on-content-click="false"
> >
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn <v-btn
@ -60,150 +59,94 @@
fluid fluid
> >
<div v-if="showNoDataMessage && !isToday" class="no-data-message"> <div v-if="showNoDataMessage && !isToday" class="no-data-message">
<v-card class="text-center pa-4" variant="outlined"> <v-card class="text-center pa-4" >
<v-card-title class="text-h6">{{ noDataMessage }}</v-card-title> <v-card-title class="text-h6">别看未来的作业了</v-card-title>
<v-card-text> <v-card-text>
<div class="text-body-1">该日期未上传作业数据</div> <div class="text-body-1">{{ noDataMessage }}</div>
</v-card-text> </v-card-text>
</v-card> </v-card>
</div> </div>
<template v-else> <template v-else>
<div v-if="showNoDataMessage && isToday" class="no-data-message"> <div class="grid-masonry" ref="gridContainer">
<v-card class="text-center pa-4" variant="outlined"> <div
<v-card-title class="text-h6">开始今天的作业</v-card-title> v-for="item in sortedItems"
<v-card-text> :key="item.key"
<div class="text-body-1 mb-4">今天还没有上传作业哦</div> class="grid-item"
<v-btn :class="{
color="primary" 'empty-card': !item.content,
@click="initializeAndOpenDialog" [`grid-row-${item.rowSpan}`]: true
> }"
开始添加作业 :style="{
</v-btn> 'grid-row-end': `span ${item.rowSpan}`,
</v-card-text> order: item.order,
</v-card> cursor: uploadLoading || downloadLoading || isPastDate ? 'not-allowed' : 'pointer',
opacity: uploadLoading || downloadLoading ? '0.7' : '1'
}"
@click="!uploadLoading && !downloadLoading && !isPastDate && 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>
<template v-else> <!-- 空科目按钮组 -->
<div class="grid-masonry" ref="gridContainer"> <v-btn-group v-if="emptySubjectDisplay === 'button'" class="empty-subjects-container">
<div <v-btn
v-for="item in sortedItems" v-for="subject in emptySubjects"
:key="item.key" :key="subject.key"
class="grid-item" variant="tonal"
:class="{ color="primary"
'empty-card': !item.content, @click="!uploadLoading && !downloadLoading && openDialog(subject.key)"
[`grid-row-${item.rowSpan}`]: true :disabled="uploadLoading || downloadLoading"
}" >
:style="{ <v-icon start>mdi-plus</v-icon>
'grid-row-end': `span ${item.rowSpan}`, {{ subject.name }}
order: item.order, </v-btn>
cursor: uploadLoading || downloadLoading ? 'not-allowed' : 'pointer', </v-btn-group>
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> </template>
</v-container> </v-container>
<!-- 出勤统计区域 --> <!-- 出勤统计区域 -->
<v-navigation-drawer <v-col
v-if="studentList.length > 1" v-if="studentList.length"
location="right" class="attendance-area"
permanent cols="1"
width="300" @click="!isPastDate ? setAttendanceArea() : null"
class="attendance-drawer"
> >
<v-card <h1>出勤</h1>
class="h-100 attendance-card" <h2>应到: {{ studentList.length }}</h2>
flat <h2>实到: {{ studentList.length - selectedSet.size }}</h2>
> <h2>请假: {{ selectedSet.size }}</h2>
<v-card-text class="text-center"> <h3 v-for="(i, index) in selectedSet" :key="'absent-' + index">
<div class="attendance-numbers"> {{ `${index + 1}. ${studentList[i]}` }}
<div class="total-number mb-4"> </h3>
<div class="text-h2 font-weight-bold">{{ studentList.length }}</div> <h2>迟到: {{ lateSet.size }}</h2>
<div class="text-overline">应到</div> <h3 v-for="(i, index) in lateSet" :key="'late-' + index">
</div> {{ `${index + 1}. ${studentList[i]}` }}
</h3>
<div class="d-flex justify-space-around"> </v-col>
<div class="status-number text-success">
<div class="text-h3 font-weight-bold">
{{ studentList.length - selectedSet.size - lateSet.size }}
</div>
<div class="text-overline">实到</div>
</div>
<div class="status-number text-error">
<div class="text-h3 font-weight-bold">
{{ selectedSet.size }}
</div>
<div class="text-overline">请假</div>
</div>
<div class="status-number text-warning">
<div class="text-h3 font-weight-bold">
{{ lateSet.size }}
</div>
<div class="text-overline">迟到</div>
</div>
</div>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn
block
color="primary"
size="large"
prepend-icon="mdi-pencil"
@click="setAttendanceArea"
>
编辑出勤
</v-btn>
</v-card-actions>
</v-card>
</v-navigation-drawer>
</div> </div>
<v-container fluid> <v-container fluid>
<v-btn <v-btn
@ -232,7 +175,9 @@
> >
<v-card border> <v-card border>
<v-card-title>{{ dialogTitle }}</v-card-title> <v-card-title>{{ dialogTitle }}</v-card-title>
<v-card-subtitle>写完后点击上传谢谢喵</v-card-subtitle> <v-card-subtitle>
{{ autoSave ? "喵?喵呜!" : "写完后点击上传谢谢喵" }}
</v-card-subtitle>
<v-card-text> <v-card-text>
<v-textarea <v-textarea
ref="inputRef" ref="inputRef"
@ -420,13 +365,6 @@
.empty-subjects-container { .empty-subjects-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.empty-subject-btn {
flex: 1;
min-width: 120px;
} }
@media (max-width: 1199px) { @media (max-width: 1199px) {
@ -440,10 +378,6 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.empty-subject-btn {
min-width: 100px;
}
.empty-card { .empty-card {
transform: scale(0.95); transform: scale(0.95);
} }
@ -635,12 +569,15 @@ export default {
return true; return true;
}); });
// // Sort items: prioritize non-empty content first
if (this.dynamicSort) { return items.sort((a, b) => {
return this.optimizeGridLayout(items); // If one item has content and the other does not, prioritize the one with content
} else { if (a.content && !b.content) return -1;
return this.fixedGridLayout(items); if (!a.content && b.content) return 1;
}
// If both have content or both are empty, sort by order
return a.order - b.order;
});
}, },
attendanceVisible() { attendanceVisible() {
return this.studentList.length > 0; return this.studentList.length > 0;
@ -670,6 +607,14 @@ export default {
dynamicSort() { dynamicSort() {
return getSetting('display.dynamicSort'); return getSetting('display.dynamicSort');
}, },
isPastDate() {
const selectedDate = new Date(this.dateString);
const today = new Date();
//
selectedDate.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
return selectedDate < today; // true
},
}, },
async mounted() { async mounted() {
@ -806,12 +751,29 @@ export default {
this.synced = true; this.synced = true;
} else { } else {
// Handle the error case // Handle the error case for today's date
this.showNoDataMessage = true; if (this.isToday && res.data.status == false) {
this.noDataMessage = res.data.msg || '未找到数据'; this.showNoDataMessage = false; // Ensure we show the empty data
this.homeworkData = {}; this.homeworkData = {
this.selectedSet.clear(); "语文": { name: "语文", content: "" },
this.lateSet.clear(); "数学": { name: "数学", content: "" },
"英语": { name: "英语", content: "" },
"物理": { name: "物理", content: "" },
"化学": { name: "化学", content: "" },
"生物": { name: "生物", content: "" },
"历史": { name: "历史", content: "" },
"地理": { name: "地理", content: "" },
"政治": { name: "政治", content: "" },
};
this.noDataMessage = ''; // Clear any previous message
} else {
// Handle other error cases
this.showNoDataMessage = true;
this.noDataMessage = res.data.msg || '未找到数据';
this.homeworkData = {};
this.selectedSet.clear();
this.lateSet.clear();
}
} }
} catch (error) { } catch (error) {
console.error('下载数据失败:', error); console.error('下载数据失败:', error);
@ -1076,24 +1038,6 @@ export default {
})); }));
}, },
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);
}
},
setAllPresent() { setAllPresent() {
this.selectedSet.clear(); this.selectedSet.clear();
this.lateSet.clear(); this.lateSet.clear();

View File

@ -1,19 +1,14 @@
<template> <template>
<v-app-bar elevation="1"> <v-app-bar elevation="1">
<template #prepend> <template #prepend>
<v-btn <v-btn icon="mdi-arrow-left" variant="text" @click="$router.push('/')" class="mr-2" />
icon="mdi-arrow-left"
variant="text"
@click="$router.push('/')"
class="mr-2"
/>
</template> </template>
<v-app-bar-title class="text-h6 font-weight-medium">设置</v-app-bar-title> <v-app-bar-title class="text-h6 font-weight-medium">设置</v-app-bar-title>
</v-app-bar> </v-app-bar>
<v-container class="py-4"> <v-container class="py-4">
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="4">
<v-card elevation="2" class="rounded-lg"> <v-card elevation="2" class="rounded-lg">
<v-card-item> <v-card-item>
<template v-slot:prepend> <template v-slot:prepend>
@ -21,46 +16,26 @@
</template> </template>
<v-card-title class="text-h6">服务器设置</v-card-title> <v-card-title class="text-h6">服务器设置</v-card-title>
</v-card-item> </v-card-item>
<v-card-text> <v-card-text>
<v-text-field <v-text-field v-model="settings.server.domain" label="服务器域名" placeholder="例如: http://example.com"
v-model="settings.server.domain" prepend-inner-icon="mdi-web" variant="outlined" density="comfortable" class="mb-4" required />
label="服务器域名" <v-text-field v-model="settings.server.classNumber" label="班号" placeholder="例如: 1 或 A"
placeholder="例如: http://example.com" prepend-inner-icon="mdi-account-group" variant="outlined" density="comfortable" required
prepend-inner-icon="mdi-web" :rules="[v => !!v || '班号不能为空', v => /^[A-Za-z0-9]+$/.test(v) || '班号只能包含字母和数字']" />
variant="outlined"
density="comfortable"
class="mb-4"
required
/>
<v-text-field
v-model="settings.server.classNumber"
label="班号"
placeholder="例如: 1 或 A"
prepend-inner-icon="mdi-account-group"
variant="outlined"
density="comfortable"
required
:rules="[v => !!v || '班号不能为空', v => /^[A-Za-z0-9]+$/.test(v) || '班号只能包含字母和数字']"
/>
</v-card-text> </v-card-text>
<v-card-actions class="px-4 pb-4"> <v-card-actions class="px-4 pb-4">
<v-btn <v-btn color="primary" prepend-icon="mdi-content-save" block @click="saveSettings('server')">
color="primary"
prepend-icon="mdi-content-save"
block
@click="saveSettings('server')"
>
保存设置 保存设置
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="12" md="6">
<v-col cols="12" md="4">
<v-card elevation="2" class="rounded-lg"> <v-card elevation="2" class="rounded-lg">
<v-card-item> <v-card-item>
<template v-slot:prepend> <template v-slot:prepend>
@ -68,44 +43,25 @@
</template> </template>
<v-card-title class="text-h6">自动刷新设置不建议启用</v-card-title> <v-card-title class="text-h6">自动刷新设置不建议启用</v-card-title>
</v-card-item> </v-card-item>
<v-card-text> <v-card-text>
<v-switch <v-switch v-model="settings.refresh.auto" label="启用自动刷新" color="primary" hide-details class="mb-4" />
v-model="settings.refresh.auto" <v-text-field v-model="settings.refresh.interval" type="number" label="刷新间隔" suffix="秒"
label="启用自动刷新" :disabled="!settings.refresh.auto" variant="outlined" density="comfortable" :rules="[
color="primary"
hide-details
class="mb-4"
/>
<v-text-field
v-model="settings.refresh.interval"
type="number"
label="刷新间隔"
suffix="秒"
:disabled="!settings.refresh.auto"
variant="outlined"
density="comfortable"
:rules="[
v => v >= 10 || '刷新间隔不能小于10秒', v => v >= 10 || '刷新间隔不能小于10秒',
v => v <= 3600 || '刷新间隔不能大于3600秒' v => v <= 3600 || '刷新间隔不能大于3600秒'
]" ]" />
/>
</v-card-text> </v-card-text>
<v-card-actions class="px-4 pb-4"> <v-card-actions class="px-4 pb-4">
<v-btn <v-btn color="primary" prepend-icon="mdi-content-save" block @click="saveSettings('refresh')">
color="primary"
prepend-icon="mdi-content-save"
block
@click="saveSettings('refresh')"
>
保存设置 保存设置
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="4">
<v-card elevation="2" class="rounded-lg"> <v-card elevation="2" class="rounded-lg">
<v-card-item> <v-card-item>
<template v-slot:prepend> <template v-slot:prepend>
@ -113,46 +69,28 @@
</template> </template>
<v-card-title class="text-h6">字体设置</v-card-title> <v-card-title class="text-h6">字体设置</v-card-title>
</v-card-item> </v-card-item>
<v-card-text> <v-card-text>
<v-text-field <v-text-field v-model="settings.font.size" type="number" label="字体大小" suffix="px" variant="outlined"
v-model="settings.font.size" density="comfortable" class="mb-4" :rules="[
type="number"
label="字体大小"
suffix="px"
variant="outlined"
density="comfortable"
class="mb-4"
:rules="[
v => v >= 16 || '字体大小不能小于16px', v => v >= 16 || '字体大小不能小于16px',
v => v <= 100 || '字体大小不能大于100px' v => v <= 100 || '字体大小不能大于100px'
]" ]" />
/>
</v-card-text> </v-card-text>
<v-card-actions class="px-4 pb-4"> <v-card-actions class="px-4 pb-4">
<v-btn <v-btn color="error" variant="outlined" prepend-icon="mdi-refresh" class="flex-grow-1"
color="error" @click="resetFontSize">
variant="outlined"
prepend-icon="mdi-refresh"
class="flex-grow-1"
@click="resetFontSize"
>
重置 重置
</v-btn> </v-btn>
<v-btn <v-btn color="primary" prepend-icon="mdi-content-save" class="flex-grow-1" @click="saveSettings('font')">
color="primary"
prepend-icon="mdi-content-save"
class="flex-grow-1"
@click="saveSettings('font')"
>
保存设置 保存设置
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="4">
<v-card elevation="2" class="rounded-lg"> <v-card elevation="2" class="rounded-lg">
<v-card-item> <v-card-item>
<template v-slot:prepend> <template v-slot:prepend>
@ -160,45 +98,24 @@
</template> </template>
<v-card-title class="text-h6">编辑设置</v-card-title> <v-card-title class="text-h6">编辑设置</v-card-title>
</v-card-item> </v-card-item>
<v-card-text> <v-card-text>
<v-switch <v-switch v-model="settings.edit.autoSave" label="启用自动保存" color="primary" hide-details class="mb-4">
v-model="settings.edit.autoSave"
label="启用自动保存"
color="primary"
hide-details
class="mb-4"
>
<template v-slot:append> <template v-slot:append>
<v-tooltip location="right"> <v-tooltip location="right">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-icon <v-icon v-bind="props" icon="mdi-help-circle-outline" size="small" class="ml-2" />
v-bind="props"
icon="mdi-help-circle-outline"
size="small"
class="ml-2"
/>
</template> </template>
编辑完成后自动上传到服务器 编辑完成后自动上传到服务器
</v-tooltip> </v-tooltip>
</template> </template>
</v-switch> </v-switch>
<v-switch <v-switch v-model="settings.edit.refreshBeforeEdit" label="编辑前自动刷新" color="primary" hide-details>
v-model="settings.edit.refreshBeforeEdit"
label="编辑前自动刷新"
color="primary"
hide-details
>
<template v-slot:append> <template v-slot:append>
<v-tooltip location="right"> <v-tooltip location="right">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-icon <v-icon v-bind="props" icon="mdi-help-circle-outline" size="small" class="ml-2" />
v-bind="props"
icon="mdi-help-circle-outline"
size="small"
class="ml-2"
/>
</template> </template>
打开编辑框前自动从服务器获取最新数据 打开编辑框前自动从服务器获取最新数据
</v-tooltip> </v-tooltip>
@ -207,117 +124,76 @@
</v-card-text> </v-card-text>
<v-card-actions class="px-4 pb-4"> <v-card-actions class="px-4 pb-4">
<v-btn <v-btn color="primary" prepend-icon="mdi-content-save" block @click="saveSettings('edit')">
color="primary"
prepend-icon="mdi-content-save"
block
@click="saveSettings('edit')"
>
保存设置 保存设置
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-col> <v-col cols="12" md="6"> </v-col> <v-col cols="12" md="4">
<v-card elevation="2" class="rounded-lg"> <v-card elevation="2" class="rounded-lg">
<v-card-item> <v-card-item>
<template v-slot:prepend> <template v-slot:prepend>
<v-icon icon="mdi-card-outline" size="large" class="mr-2" /> <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-switch
v-model="settings.display.dynamicSort"
label="启用动态排序"
hint="动态排序会根据内容长度自动调整卡片位置以优化显示效果"
persistent-hint
class="mb-4"
@change="saveSettings('display')"
>
<template v-slot:append>
<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>
<div>
<p>启用根据内容长度动态调整位置</p>
<p>关闭按语数英/物化生/政史地固定排列</p>
</div>
</v-tooltip>
</template> </template>
</v-switch> <v-card-title class="text-h6">显示设置</v-card-title>
</v-card-item>
<v-divider class="my-4" /> <v-card-text>
<v-switch v-model="settings.display.dynamicSort" label="启用动态排序" hint="动态排序会根据内容长度自动调整卡片位置以优化显示效果"
<v-radio-group persistent-hint class="mb-4" @change="saveSettings('display')">
v-model="settings.display.emptySubjectDisplay" <template v-slot:append>
label="空作业显示方式" <v-tooltip location="right">
class="mt-4" <template v-slot:activator="{ props }">
@change="saveSettings('display')" <v-icon v-bind="props" icon="mdi-help-circle-outline" size="small" class="ml-2" />
> </template>
<v-radio <div>
value="card" <p>启用根据内容长度动态调整位置</p>
label="显示为空卡片" <p>关闭按语数英/物化生/政史地固定排列</p>
> </div>
<template v-slot:label> </v-tooltip>
<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> </template>
</v-radio> </v-switch>
<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-divider class="my-4" />
<v-btn
color="primary" <v-radio-group v-model="settings.display.emptySubjectDisplay" label="空作业显示方式" class="mt-4"
prepend-icon="mdi-content-save" @change="saveSettings('display')">
block <v-radio value="card" label="显示为空卡片">
@click="saveSettings('display')" <template v-slot:label>
> <div class="d-flex align-center">
保存设置 显示为空卡片
</v-btn> <v-tooltip location="right">
</v-card-actions> <template v-slot:activator="{ props }">
</v-card> <v-icon v-bind="props" icon="mdi-help-circle-outline" size="small" class="ml-2" />
</v-col> </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="saveSettings('display')">
保存设置
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<v-col cols="12"> <v-col cols="12">
<v-card elevation="2" class="rounded-lg"> <v-card elevation="2" class="rounded-lg">
<v-card-item> <v-card-item>
@ -326,32 +202,17 @@
</template> </template>
<v-card-title class="text-h6">学生列表设置</v-card-title> <v-card-title class="text-h6">学生列表设置</v-card-title>
<template v-slot:append> <template v-slot:append>
<v-btn <v-btn :color="showAdvancedEdit ? 'primary' : undefined" variant="text" prepend-icon="mdi-code-braces"
:color="showAdvancedEdit ? 'primary' : undefined" @click="showAdvancedEdit = !showAdvancedEdit">
variant="text"
prepend-icon="mdi-code-braces"
@click="showAdvancedEdit = !showAdvancedEdit"
>
{{ showAdvancedEdit ? '返回基础编辑' : '高级编辑' }} {{ showAdvancedEdit ? '返回基础编辑' : '高级编辑' }}
</v-btn> </v-btn>
</template> </template>
</v-card-item> </v-card-item>
<v-card-text> <v-card-text>
<v-progress-linear <v-progress-linear v-if="studentsLoading" indeterminate color="primary" class="mb-4" />
v-if="studentsLoading"
indeterminate
color="primary"
class="mb-4"
/>
<v-alert <v-alert v-if="studentsError" type="error" variant="tonal" closable class="mb-4">
v-if="studentsError"
type="error"
variant="tonal"
closable
class="mb-4"
>
{{ studentsError }} {{ studentsError }}
</v-alert> </v-alert>
@ -359,137 +220,68 @@
<div v-if="!showAdvancedEdit"> <div v-if="!showAdvancedEdit">
<v-row class="mb-6"> <v-row class="mb-6">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field v-model="newStudent" label="添加学生" placeholder="输入学生姓名后回车添加"
v-model="newStudent" prepend-inner-icon="mdi-account-plus" variant="outlined" hide-details @keyup.enter="addStudent">
label="添加学生"
placeholder="输入学生姓名后回车添加"
prepend-inner-icon="mdi-account-plus"
variant="outlined"
hide-details
@keyup.enter="addStudent"
>
<template #append> <template #append>
<v-btn <v-btn icon="mdi-plus" variant="text" color="primary" :disabled="!newStudent.trim()"
icon="mdi-plus" @click="addStudent" />
variant="text"
color="primary"
:disabled="!newStudent.trim()"
@click="addStudent"
/>
</template> </template>
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col <v-col v-for="(student, index) in studentsList" :key="index" cols="12" sm="6" md="4" lg="3">
v-for="(student, index) in studentsList"
:key="index"
cols="12"
sm="6"
md="4"
lg="3"
>
<v-hover v-slot="{ isHovering, props }"> <v-hover v-slot="{ isHovering, props }">
<v-card <v-card v-bind="props" :elevation="isMobile ? 1 : (isHovering ? 4 : 1)" :class="[
v-bind="props" 'student-card',
:elevation="isMobile ? 1 : (isHovering ? 4 : 1)" {
:class="[ 'bg-primary-subtle': isHovering && !isMobile,
'student-card', 'mobile': isMobile
{ }
'bg-primary-subtle': isHovering && !isMobile, ]" border>
'mobile': isMobile
}
]"
border
>
<v-card-text class="d-flex align-center pa-3"> <v-card-text class="d-flex align-center pa-3">
<v-menu <v-menu location="bottom" :open-on-hover="!isMobile" :open-on-long-press="isMobile">
location="bottom"
:open-on-hover="!isMobile"
:open-on-long-press="isMobile"
>
<template v-slot:activator="{ props: menuProps }"> <template v-slot:activator="{ props: menuProps }">
<v-btn <v-btn variant="tonal" size="small" class="mr-3 font-weight-medium" v-bind="menuProps">
variant="tonal"
size="small"
class="mr-3 font-weight-medium"
v-bind="menuProps"
>
{{ index + 1 }} {{ index + 1 }}
</v-btn> </v-btn>
</template> </template>
<v-list density="compact" nav> <v-list density="compact" nav>
<v-list-item <v-list-item prepend-icon="mdi-arrow-up-bold" :disabled="index === 0"
prepend-icon="mdi-arrow-up-bold" @click="moveToTop(index)">
:disabled="index === 0"
@click="moveToTop(index)"
>
置顶 置顶
</v-list-item> </v-list-item>
<v-divider /> <v-divider />
<v-list-item <v-list-item prepend-icon="mdi-arrow-up" :disabled="index === 0"
prepend-icon="mdi-arrow-up" @click="moveStudent(index, 'up')">
:disabled="index === 0"
@click="moveStudent(index, 'up')"
>
上移 上移
</v-list-item> </v-list-item>
<v-list-item <v-list-item prepend-icon="mdi-arrow-down" :disabled="index === studentsList.length - 1"
prepend-icon="mdi-arrow-down" @click="moveStudent(index, 'down')">
:disabled="index === studentsList.length - 1"
@click="moveStudent(index, 'down')"
>
下移 下移
</v-list-item> </v-list-item>
<v-divider /> <v-divider />
<v-list-item <v-list-item prepend-icon="mdi-format-list-numbered" @click="setStudentNumber(index)">
prepend-icon="mdi-format-list-numbered"
@click="setStudentNumber(index)"
>
设置序号 设置序号
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
<v-text-field <v-text-field v-if="editingIndex === index" v-model="editingName" density="compact"
v-if="editingIndex === index" variant="underlined" hide-details class="flex-grow-1" @keyup.enter="saveEdit"
v-model="editingName" @blur="saveEdit" autofocus />
density="compact" <span v-else class="text-body-1 flex-grow-1"
variant="underlined"
hide-details
class="flex-grow-1"
@keyup.enter="saveEdit"
@blur="saveEdit"
autofocus
/>
<span
v-else
class="text-body-1 flex-grow-1"
@click="isMobile ? startEdit(index, student) : null" @click="isMobile ? startEdit(index, student) : null"
@dblclick="!isMobile ? startEdit(index, student) : null" @dblclick="!isMobile ? startEdit(index, student) : null">
>
{{ student }} {{ student }}
</span> </span>
<div <div class="d-flex gap-1 action-buttons" :class="{ 'opacity-100': isHovering || isMobile }">
class="d-flex gap-1 action-buttons" <v-btn icon="mdi-pencil" variant="text" color="primary" size="small"
:class="{'opacity-100': isHovering || isMobile}" @click="startEdit(index, student)" />
> <v-btn icon="mdi-delete" variant="text" color="error" size="small"
<v-btn @click="confirmDelete(index)" />
icon="mdi-pencil"
variant="text"
color="primary"
size="small"
@click="startEdit(index, student)"
/>
<v-btn
icon="mdi-delete"
variant="text"
color="error"
size="small"
@click="confirmDelete(index)"
/>
</div> </div>
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -501,39 +293,19 @@
<v-expand-transition> <v-expand-transition>
<div v-if="showAdvancedEdit" class="pt-2"> <div v-if="showAdvancedEdit" class="pt-2">
<v-textarea <v-textarea v-model="students" label="批量编辑学生列表" placeholder="每行输入一个学生姓名" hint="使用文本编辑模式批量编辑学生名单"
v-model="students" persistent-hint variant="outlined" rows="10" />
label="批量编辑学生列表"
placeholder="每行输入一个学生姓名"
hint="使用文本编辑模式批量编辑学生名单"
persistent-hint
variant="outlined"
rows="10"
/>
</div> </div>
</v-expand-transition> </v-expand-transition>
<v-row class="mt-6"> <v-row class="mt-6">
<v-col cols="12" class="d-flex gap-2"> <v-col cols="12" class="d-flex gap-2">
<v-btn <v-btn color="primary" prepend-icon="mdi-content-save" size="large" :loading="studentsLoading"
color="primary" :disabled="studentsLoading" @click="saveStudents">
prepend-icon="mdi-content-save"
size="large"
:loading="studentsLoading"
:disabled="studentsLoading"
@click="saveStudents"
>
保存学生列表 保存学生列表
</v-btn> </v-btn>
<v-btn <v-btn color="error" variant="outlined" prepend-icon="mdi-refresh" size="large"
color="error" :loading="studentsLoading" :disabled="studentsLoading" @click="reloadStudentList">
variant="outlined"
prepend-icon="mdi-refresh"
size="large"
:loading="studentsLoading"
:disabled="studentsLoading"
@click="reloadStudentList"
>
重置列表 重置列表
</v-btn> </v-btn>
</v-col> </v-col>
@ -554,45 +326,25 @@
<v-row justify="center" align="center"> <v-row justify="center" align="center">
<v-col cols="12" md="8" class="text-center"> <v-col cols="12" md="8" class="text-center">
<v-avatar size="120" class="mb-4"> <v-avatar size="120" class="mb-4">
<v-img <v-img src="https://avatars.githubusercontent.com/u/88357633?v=4" alt="作者头像" />
src="https://avatars.githubusercontent.com/u/88357633?v=4"
alt="作者头像"
/>
</v-avatar> </v-avatar>
<h2 class="text-h5 mb-2">HomeworkPage</h2> <h2 class="text-h5 mb-2">HomeworkPage</h2>
<p class="text-body-1 mb-4"> <p class="text-body-1 mb-4">
<a <a href="https://github.com/sunwuyuan" target="_blank"
href="https://github.com/sunwuyuan" class="text-decoration-none font-weight-medium">Sunwuyuan</a> 开发
target="_blank"
class="text-decoration-none font-weight-medium"
>Sunwuyuan</a> 开发
</p> </p>
<div class="d-flex justify-center gap-2 flex-wrap"> <div class="d-flex justify-center gap-2 flex-wrap">
<v-btn <v-btn color="primary" variant="outlined" href="https://github.com/SunWuyuan/homeworkpage-frontend"
color="primary" target="_blank" prepend-icon="mdi-github">
variant="outlined"
href="https://github.com/SunWuyuan/homeworkpage-frontend"
target="_blank"
prepend-icon="mdi-github"
>
前端 GitHub 前端 GitHub
</v-btn> </v-btn>
<v-btn <v-btn color="primary" variant="outlined" href="https://github.com/SunWuyuan/homeworkpage-backend"
color="primary" target="_blank" prepend-icon="mdi-github">
variant="outlined"
href="https://github.com/SunWuyuan/homeworkpage-backend"
target="_blank"
prepend-icon="mdi-github"
>
后端 GitHub 后端 GitHub
</v-btn> </v-btn>
<v-btn <v-btn color="primary" variant="outlined"
color="primary" href="https://github.com/SunWuyuan/homeworkpage-backend/issues" target="_blank"
variant="outlined" prepend-icon="mdi-bug">
href="https://github.com/SunWuyuan/homeworkpage-backend/issues"
target="_blank"
prepend-icon="mdi-bug"
>
报告问题 报告问题
</v-btn> </v-btn>
</div> </div>
@ -622,11 +374,7 @@
<v-btn color="primary" variant="text" @click="deleteDialog = false"> <v-btn color="primary" variant="text" @click="deleteDialog = false">
取消 取消
</v-btn> </v-btn>
<v-btn <v-btn color="error" variant="text" @click="removeStudent(studentToDelete?.index)">
color="error"
variant="text"
@click="removeStudent(studentToDelete?.index)"
>
删除 删除
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@ -637,17 +385,11 @@
<v-card> <v-card>
<v-card-title>设置序号</v-card-title> <v-card-title>设置序号</v-card-title>
<v-card-text> <v-card-text>
<v-text-field <v-text-field v-model="newPosition" type="number" label="新序号" :rules="[
v-model="newPosition" v => !!v || '序号不能为空',
type="number" v => v > 0 || '序号必须大于0',
label="新序号" v => v <= studentsList.length || `序号不能大于${studentsList.length}`
:rules="[ ]" @keyup.enter="applyNewPosition" />
v => !!v || '序号不能为空',
v => v > 0 || '序号必须大于0',
v => v <= studentsList.length || `序号不能大于${studentsList.length}`
]"
@keyup.enter="applyNewPosition"
/>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
@ -665,7 +407,7 @@
<script> <script>
import axios from 'axios'; import axios from 'axios';
import { useDisplay } from 'vuetify'; import { useDisplay } from 'vuetify';
import { import {
getSetting, getSetting,
setSetting, setSetting,
resetSetting, resetSetting,
@ -720,7 +462,7 @@ export default {
studentsError: null, studentsError: null,
}; };
}, },
mounted() { mounted() {
this.loadAllSettings(); this.loadAllSettings();
this.unwatchSettings = watchSettings(() => { this.unwatchSettings = watchSettings(() => {
@ -810,7 +552,7 @@ export default {
const domain = getSetting('server.domain'); const domain = getSetting('server.domain');
const classNum = getSetting('server.classNumber'); const classNum = getSetting('server.classNumber');
if (!domain || !classNum) { if (!domain || !classNum) {
throw new Error('请先设置服务器域名和班号'); throw new Error('请先设置服务器域名和班号');
} }
@ -835,7 +577,7 @@ export default {
try { try {
const domain = getSetting('server.domain'); const domain = getSetting('server.domain');
const classNum = getSetting('server.classNumber'); const classNum = getSetting('server.classNumber');
if (!domain || !classNum) { if (!domain || !classNum) {
throw new Error('请先设置服务器域名和班号'); throw new Error('请先设置服务器域名和班号');
} }
@ -871,7 +613,7 @@ export default {
if (this.editingIndex !== -1 && this.editingIndex !== index) { if (this.editingIndex !== -1 && this.editingIndex !== index) {
this.saveEdit(); this.saveEdit();
} }
this.editingIndex = index; this.editingIndex = index;
this.editingName = name; this.editingName = name;
}, },
@ -901,9 +643,9 @@ export default {
moveStudent(index, direction) { moveStudent(index, direction) {
const newIndex = direction === 'up' ? index - 1 : index + 1; const newIndex = direction === 'up' ? index - 1 : index + 1;
if (newIndex >= 0 && newIndex < this.studentsList.length) { if (newIndex >= 0 && newIndex < this.studentsList.length) {
[this.studentsList[index], this.studentsList[newIndex]] = [this.studentsList[index], this.studentsList[newIndex]] =
[this.studentsList[newIndex], this.studentsList[index]]; [this.studentsList[newIndex], this.studentsList[index]];
if (this.settings.edit.autoSave) { if (this.settings.edit.autoSave) {
this.saveStudents(); this.saveStudents();
} }
@ -939,7 +681,7 @@ export default {
const student = this.studentsList[index]; const student = this.studentsList[index];
this.studentsList.splice(index, 1); this.studentsList.splice(index, 1);
this.studentsList.unshift(student); this.studentsList.unshift(student);
if (this.settings.edit.autoSave) { if (this.settings.edit.autoSave) {
this.saveStudents(); this.saveStudents();
} }
@ -1006,7 +748,7 @@ export default {
.v-container { .v-container {
padding: 12px; padding: 12px;
} }
.v-col { .v-col {
padding: 8px; padding: 8px;
} }
@ -1029,11 +771,11 @@ export default {
.v-col { .v-col {
padding: 6px !important; padding: 6px !important;
} }
.student-card { .student-card {
margin-bottom: 4px; margin-bottom: 4px;
} }
.action-buttons { .action-buttons {
opacity: 1; opacity: 1;
} }
@ -1046,4 +788,4 @@ export default {
.student-card:active { .student-card:active {
background-color: rgb(var(--v-theme-primary), 0.05); background-color: rgb(var(--v-theme-primary), 0.05);
} }
</style> </style>

View File

@ -72,12 +72,12 @@ const settingsDefinitions = {
// 编辑设置 // 编辑设置
'edit.autoSave': { 'edit.autoSave': {
type: 'boolean', type: 'boolean',
default: false, default: true,
description: '是否启用自动保存' description: '是否启用自动保存'
}, },
'edit.refreshBeforeEdit': { 'edit.refreshBeforeEdit': {
type: 'boolean', type: 'boolean',
default: false, default: true,
description: '编辑前是否自动刷新' description: '编辑前是否自动刷新'
} }
}; };