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-01 20:31:34 +08:00
parent a3decdb6d8
commit e31578ee10
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
2 changed files with 565 additions and 125 deletions

View File

@ -4,7 +4,7 @@
"type": "module", "type": "module",
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --fix" "lint": "eslint . --fix"

View File

@ -1,58 +1,253 @@
<template> <template>
<v-app-bar> <v-app-bar elevation="1">
<template #prepend> <template #prepend>
<v-btn icon="mdi-arrow-left" variant="text" @click="$router.push('/')" /> <v-btn
icon="mdi-arrow-left"
variant="text"
@click="$router.push('/')"
class="mr-2"
/>
</template> </template>
<v-app-bar-title>设置</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> <v-container class="py-4">
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12" md="6">
<v-card> <v-card elevation="2" class="rounded-lg">
<v-card-title>服务器设置</v-card-title> <v-card-item>
<template v-slot:prepend>
<v-icon icon="mdi-server" size="large" class="mr-2" />
</template>
<v-card-title class="text-h6">服务器设置</v-card-title>
</v-card-item>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="serverDomain" v-model="serverDomain"
label="服务器域名" label="服务器域名"
placeholder="例如: http://example.com" placeholder="例如: http://example.com"
prepend-inner-icon="mdi-web"
variant="outlined"
density="comfortable"
class="mb-4"
required required
/> />
<v-text-field <v-text-field
v-model="classNumber" v-model="classNumber"
label="班号" label="班号"
placeholder="例如: 1 或 A" placeholder="例如: 1 或 A"
prepend-inner-icon="mdi-account-group"
variant="outlined"
density="comfortable"
required required
:rules="[v => !!v || '班号不能为空', v => /^[A-Za-z0-9]+$/.test(v) || '班号只能包含字母和数字']" :rules="[v => !!v || '班号不能为空', v => /^[A-Za-z0-9]+$/.test(v) || '班号只能包含字母和数字']"
/> />
<v-btn color="primary" @click="saveServerSettings">保存</v-btn>
</v-card-text> </v-card-text>
<v-card-actions class="px-4 pb-4">
<v-btn
color="primary"
prepend-icon="mdi-content-save"
block
@click="saveServerSettings"
>
保存设置
</v-btn>
</v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="12">
<v-card>
<v-card-title class="d-flex align-center"> <v-col cols="12" md="6">
学生列表设置 <v-card elevation="2" class="rounded-lg">
<v-spacer /> <v-card-item>
<template v-slot:prepend>
<v-icon icon="mdi-refresh" 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="autoRefresh"
label="启用自动刷新"
color="primary"
hide-details
class="mb-4"
/>
<v-text-field
v-model="refreshInterval"
type="number"
label="刷新间隔"
suffix="秒"
:disabled="!autoRefresh"
variant="outlined"
density="comfortable"
:rules="[
v => v >= 10 || '刷新间隔不能小于10秒',
v => v <= 3600 || '刷新间隔不能大于3600秒'
]"
/>
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-btn <v-btn
color="primary"
prepend-icon="mdi-content-save"
block
@click="saveRefreshSettings"
>
保存设置
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card elevation="2" class="rounded-lg">
<v-card-item>
<template v-slot:prepend>
<v-icon icon="mdi-format-size" size="large" class="mr-2" />
</template>
<v-card-title class="text-h6">字体设置</v-card-title>
</v-card-item>
<v-card-text>
<v-text-field
v-model="fontSize"
type="number"
label="字体大小"
suffix="px"
variant="outlined"
density="comfortable"
class="mb-4"
:rules="[
v => v >= 16 || '字体大小不能小于16px',
v => v <= 100 || '字体大小不能大于100px'
]"
/>
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-btn
color="error"
variant="outlined"
prepend-icon="mdi-refresh"
class="flex-grow-1"
@click="resetFontSize"
>
重置
</v-btn>
<v-btn
color="primary"
prepend-icon="mdi-content-save"
class="flex-grow-1"
@click="saveFontSize"
>
保存设置
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card elevation="2" class="rounded-lg">
<v-card-item>
<template v-slot:prepend>
<v-icon icon="mdi-pencil-cog" 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="autoSave"
label="启用自动保存"
color="primary"
hide-details
class="mb-4"
>
<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>
编辑完成后自动上传到服务器
</v-tooltip>
</template>
</v-switch>
<v-switch
v-model="refreshBeforeEdit"
label="编辑前自动刷新"
color="primary"
hide-details
>
<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>
打开编辑框前自动从服务器获取最新数据
</v-tooltip>
</template>
</v-switch>
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-btn
color="primary"
prepend-icon="mdi-content-save"
block
@click="saveEditSettings"
>
保存设置
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<v-col cols="12">
<v-card elevation="2" class="rounded-lg">
<v-card-item>
<template v-slot:prepend>
<v-icon icon="mdi-account-multiple" size="large" class="mr-2" />
</template>
<v-card-title class="text-h6">学生列表设置</v-card-title>
<template v-slot:append>
<v-btn
:color="showAdvancedEdit ? 'primary' : undefined"
variant="text" variant="text"
prepend-icon="mdi-code-braces" prepend-icon="mdi-code-braces"
@click="showAdvancedEdit = !showAdvancedEdit" @click="showAdvancedEdit = !showAdvancedEdit"
> >
{{ showAdvancedEdit ? '基础编辑' : '高级编辑' }} {{ showAdvancedEdit ? '返回基础编辑' : '高级编辑' }}
</v-btn> </v-btn>
</v-card-title> </template>
</v-card-item>
<v-card-text> <v-card-text>
<v-expand-transition> <v-expand-transition>
<div v-if="!showAdvancedEdit"> <div v-if="!showAdvancedEdit">
<v-row class="mb-4"> <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" v-model="newStudent"
label="添加学生" label="添加学生"
placeholder="输入学生姓名" placeholder="输入学生姓名后回车添加"
prepend-inner-icon="mdi-account-plus"
variant="outlined"
hide-details hide-details
@keyup.enter="addStudent" @keyup.enter="addStudent"
> >
@ -60,6 +255,7 @@
<v-btn <v-btn
icon="mdi-plus" icon="mdi-plus"
variant="text" variant="text"
color="primary"
:disabled="!newStudent.trim()" :disabled="!newStudent.trim()"
@click="addStudent" @click="addStudent"
/> />
@ -77,134 +273,162 @@
md="4" md="4"
lg="3" lg="3"
> >
<v-card variant="outlined" class="student-card"> <v-hover v-slot="{ isHovering, props }">
<v-card-text class="d-flex align-center"> <v-card
<span class="mr-2">{{ index + 1 }}.</span> v-bind="props"
<span class="text-body-1 flex-grow-1">{{ student }}</span> :elevation="isMobile ? 1 : (isHovering ? 4 : 1)"
:class="[
'student-card',
{
'bg-primary-subtle': isHovering && !isMobile,
'mobile': isMobile
}
]"
border
>
<v-card-text class="d-flex align-center pa-3">
<v-menu
location="bottom"
:open-on-hover="!isMobile"
:open-on-long-press="isMobile"
>
<template v-slot:activator="{ props: menuProps }">
<v-btn
variant="tonal"
size="small"
class="mr-3 font-weight-medium"
v-bind="menuProps"
>
{{ index + 1 }}
</v-btn>
</template>
<v-list density="compact" nav>
<v-list-item
prepend-icon="mdi-arrow-up-bold"
:disabled="index === 0"
@click="moveToTop(index)"
>
置顶
</v-list-item>
<v-divider />
<v-list-item
prepend-icon="mdi-arrow-up"
:disabled="index === 0"
@click="moveStudent(index, 'up')"
>
上移
</v-list-item>
<v-list-item
prepend-icon="mdi-arrow-down"
:disabled="index === studentsList.length - 1"
@click="moveStudent(index, 'down')"
>
下移
</v-list-item>
<v-divider />
<v-list-item
prepend-icon="mdi-format-list-numbered"
@click="setStudentNumber(index)"
>
设置序号
</v-list-item>
</v-list>
</v-menu>
<v-text-field
v-if="editingIndex === index"
v-model="editingName"
density="compact"
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"
@dblclick="!isMobile ? startEdit(index, student) : null"
>
{{ student }}
</span>
<div
class="d-flex gap-1 action-buttons"
:class="{'opacity-100': isHovering || isMobile}"
>
<v-btn
icon="mdi-pencil"
variant="text"
color="primary"
size="small"
@click="startEdit(index, student)"
/>
<v-btn <v-btn
icon="mdi-delete" icon="mdi-delete"
variant="text" variant="text"
color="error" color="error"
size="small" size="small"
@click="removeStudent(index)" @click="confirmDelete(index)"
/> />
</div>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-hover>
</v-col> </v-col>
</v-row> </v-row>
</div> </div>
</v-expand-transition> </v-expand-transition>
<v-expand-transition> <v-expand-transition>
<div v-if="showAdvancedEdit"> <div v-if="showAdvancedEdit" class="pt-2">
<v-textarea <v-textarea
v-model="students" v-model="students"
label="学生列表(每行一个名字)" label="批量编辑学生列表"
placeholder="每行输入一个学生姓名"
hint="使用文本编辑模式批量编辑学生名单" hint="使用文本编辑模式批量编辑学生名单"
persistent-hint persistent-hint
variant="outlined"
rows="10"
/> />
</div> </div>
</v-expand-transition> </v-expand-transition>
<v-row class="mt-4"> <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" color="primary"
prepend-icon="mdi-content-save" prepend-icon="mdi-content-save"
size="large"
@click="saveStudents" @click="saveStudents"
> >
保存 保存学生列表
</v-btn> </v-btn>
<v-btn <v-btn
color="error" color="error"
variant="outlined" variant="outlined"
prepend-icon="mdi-refresh" prepend-icon="mdi-refresh"
size="large"
@click="reloadStudentList" @click="reloadStudentList"
> >
重置 重置列表
</v-btn> </v-btn>
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="12"> <v-col cols="12">
<v-card disabled> <v-card elevation="2" class="rounded-lg">
<v-card-title>自动刷新设置</v-card-title> <v-card-item>
<v-card-text> <template v-slot:prepend>
<v-switch v-model="autoRefresh" label="启用自动刷新"/> <v-icon icon="mdi-information" size="large" class="mr-2" />
<v-text-field </template>
v-model="refreshInterval" <v-card-title class="text-h6">关于</v-card-title>
type="number" </v-card-item>
label="刷新间隔(秒)"
:disabled="!autoRefresh"
/>
<v-btn color="primary" @click="saveRefreshSettings">保存</v-btn>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12">
<v-card>
<v-card-title>字体设置</v-card-title>
<v-card-text>
<v-row align="center">
<v-col>
<v-text-field
v-model="fontSize"
type="number"
label="字体大小"
suffix="px"
:rules="[
v => v >= 16 || '字体大小不能小于16px',
v => v <= 100 || '字体大小不能大于100px'
]"
/>
</v-col>
<v-col cols="auto">
<v-btn color="primary" @click="saveFontSize">保存</v-btn>
</v-col>
<v-col cols="auto">
<v-btn color="error" @click="resetFontSize">重置</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12">
<v-card>
<v-card-title>编辑设置</v-card-title>
<v-card-text>
<v-row>
<v-col>
<v-switch
v-model="autoSave"
label="启用自动保存"
hint="编辑完成后自动上传到服务器"
persistent-hint
@change="saveEditSettings"
/>
</v-col>
</v-row>
<v-row>
<v-col>
<v-switch
v-model="refreshBeforeEdit"
label="编辑前自动刷新"
hint="打开编辑框前自动从服务器获取最新数据"
persistent-hint
@change="saveEditSettings"
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12">
<v-card>
<v-card-text> <v-card-text>
<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">
@ -216,9 +440,13 @@
</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 href="https://github.com/sunwuyuan" target="_blank" class="text-decoration-none">Sunwuyuan</a> 开发 <a
href="https://github.com/sunwuyuan"
target="_blank"
class="text-decoration-none font-weight-medium"
>Sunwuyuan</a> 开发
</p> </p>
<div class="d-flex justify-center gap-2"> <div class="d-flex justify-center gap-2 flex-wrap">
<v-btn <v-btn
color="primary" color="primary"
variant="outlined" variant="outlined"
@ -240,13 +468,15 @@
<v-btn <v-btn
color="primary" color="primary"
variant="outlined" variant="outlined"
href="https://github.com/SunWuyuan/homeworkpage-backend" href="https://github.com/SunWuyuan/homeworkpage-backend/issues"
target="_blank"
prepend-icon="mdi-bug"
> >
报告问题 报告问题
</v-btn> </v-btn>
</div> </div>
<p class="mt-4 text-caption"> <p class="mt-4 text-caption text-medium-emphasis">
GPL License GPL License © 2024
</p> </p>
</v-col> </v-col>
</v-row> </v-row>
@ -259,12 +489,68 @@
<v-snackbar v-model="snackbar"> <v-snackbar v-model="snackbar">
{{ snackbarText }} {{ snackbarText }}
</v-snackbar> </v-snackbar>
<v-dialog v-model="deleteDialog" max-width="300">
<v-card>
<v-card-title>确认删除</v-card-title>
<v-card-text>
确定要删除学生 "{{ studentToDelete?.name }}"
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" variant="text" @click="deleteDialog = false">
取消
</v-btn>
<v-btn
color="error"
variant="text"
@click="removeStudent(studentToDelete?.index)"
>
删除
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="numberDialog" max-width="300">
<v-card>
<v-card-title>设置序号</v-card-title>
<v-card-text>
<v-text-field
v-model="newPosition"
type="number"
label="新序号"
:rules="[
v => !!v || '序号不能为空',
v => v > 0 || '序号必须大于0',
v => v <= studentsList.length || `序号不能大于${studentsList.length}`
]"
@keyup.enter="applyNewPosition"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" variant="text" @click="numberDialog = false">
取消
</v-btn>
<v-btn color="primary" @click="applyNewPosition">
确定
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template> </template>
<script> <script>
import axios from 'axios'; import axios from 'axios';
import { useDisplay } from 'vuetify';
export default { export default {
setup() {
const { mobile } = useDisplay();
return { isMobile: mobile };
},
data() { data() {
return { return {
serverDomain: '', serverDomain: '',
@ -280,6 +566,15 @@ export default {
refreshBeforeEdit: false, refreshBeforeEdit: false,
showAdvancedEdit: false, showAdvancedEdit: false,
newStudent: '', newStudent: '',
editingIndex: -1,
editingName: '',
deleteDialog: false,
studentToDelete: null,
numberDialog: false,
newPosition: '',
studentToMove: null,
touchStartTime: 0,
touchTimeout: null,
}; };
}, },
@ -426,7 +721,13 @@ export default {
}, },
removeStudent(index) { removeStudent(index) {
if (index !== undefined) {
this.studentsList.splice(index, 1); this.studentsList.splice(index, 1);
this.deleteDialog = false;
this.studentToDelete = null;
this.synced = false;
if (this.autoSave) this.saveStudents();
}
}, },
reloadStudentList() { reloadStudentList() {
@ -443,20 +744,159 @@ export default {
this.snackbarText = text; this.snackbarText = text;
this.snackbar = true; this.snackbar = true;
}, },
startEdit(index, name) {
if (this.editingIndex !== -1 && this.editingIndex !== index) {
this.saveEdit();
}
this.editingIndex = index;
this.editingName = name;
},
saveEdit() {
if (this.editingIndex !== -1) {
const newName = this.editingName.trim();
if (newName && newName !== this.studentsList[this.editingIndex]) {
this.studentsList[this.editingIndex] = newName;
if (this.autoSave) {
this.saveStudents();
}
}
this.editingIndex = -1;
this.editingName = '';
}
},
confirmDelete(index) {
this.studentToDelete = {
index,
name: this.studentsList[index]
};
this.deleteDialog = true;
},
moveStudent(index, direction) {
const newIndex = direction === 'up' ? index - 1 : index + 1;
if (newIndex >= 0 && newIndex < this.studentsList.length) {
[this.studentsList[index], this.studentsList[newIndex]] =
[this.studentsList[newIndex], this.studentsList[index]];
if (this.autoSave) {
this.saveStudents();
}
}
},
setStudentNumber(index) {
this.studentToMove = index;
this.newPosition = String(index + 1);
this.numberDialog = true;
},
applyNewPosition() {
const newPos = parseInt(this.newPosition) - 1;
if (
this.studentToMove !== null &&
newPos >= 0 &&
newPos < this.studentsList.length &&
newPos !== this.studentToMove
) {
const student = this.studentsList[this.studentToMove];
this.studentsList.splice(this.studentToMove, 1);
this.studentsList.splice(newPos, 0, student);
this.synced = false;
if (this.autoSave) this.saveStudents();
}
this.numberDialog = false;
this.studentToMove = null;
this.newPosition = '';
},
moveToTop(index) {
if (index > 0) {
const student = this.studentsList[index];
this.studentsList.splice(index, 1);
this.studentsList.unshift(student);
if (this.autoSave) {
this.saveStudents();
}
}
},
}, },
}; };
</script> </script>
<style scoped> <style scoped>
.student-card { .student-card {
transition: all 0.3s ease; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
.student-card:hover { .bg-primary-subtle {
background-color: rgba(var(--v-theme-primary), 0.05); background-color: rgb(var(--v-theme-primary), 0.05);
}
.action-buttons {
transition: opacity 0.2s ease;
opacity: 0;
}
.gap-1 {
gap: 4px;
} }
.gap-2 { .gap-2 {
gap: 8px; gap: 8px;
} }
.student-card .v-text-field {
margin: 0;
padding: 0;
}
@media (max-width: 600px) {
.v-container {
padding: 12px;
}
.v-col {
padding: 8px;
}
}
.student-card.mobile {
margin-bottom: 8px;
}
.student-card.mobile .v-btn {
min-width: 40px;
min-height: 40px;
}
.student-card.mobile .v-text-field {
font-size: 16px;
}
@media (max-width: 600px) {
.v-col {
padding: 6px !important;
}
.student-card {
margin-bottom: 4px;
}
.action-buttons {
opacity: 1;
}
}
.student-card {
-webkit-tap-highlight-color: transparent;
}
.student-card:active {
background-color: rgb(var(--v-theme-primary), 0.05);
}
</style> </style>