1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-07 21:13:11 +00:00
Classworks/src/components/settings/StudentListCard.vue
copilot-swe-agent[bot] 511ddc358e chore: Remove all unnecessary formatting changes from previous commits
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2025-11-28 12:53:48 +00:00

470 lines
14 KiB
Vue

<template>
<v-card
:class="{ 'unsaved-changes': unsavedChanges }"
:color="unsavedChanges ? 'warning-subtle' : undefined"
border
>
<v-card-item>
<template #prepend>
<v-icon class="mr-2" icon="mdi-account-group" size="large"/>
</template>
<v-card-title class="text-h6">学生列表</v-card-title>
<template #append>
<unsaved-warning :show="unsavedChanges" message="有未保存的更改"/>
<v-btn
:disabled="modelValue.list.length === 0"
class="mr-2"
prepend-icon="mdi-sort-alphabetical-variant"
variant="text"
@click="sortStudentsByPinyin"
>
按姓名首字母排序
</v-btn>
<v-btn
:color="modelValue.advanced ? 'primary' : undefined"
prepend-icon="mdi-code-braces"
variant="text"
@click="toggleAdvanced"
>
{{ modelValue.advanced ? "返回基础编辑" : "高级编辑" }}
</v-btn>
</template>
</v-card-item>
<v-card-text>
<v-progress-linear
v-if="loading"
class="mb-4"
color="primary"
indeterminate
/>
<v-alert v-if="error" class="mb-4" closable type="error" variant="tonal">
{{ error }}
</v-alert>
<v-expand-transition>
<!-- 普通编辑模式 -->
<div v-if="!modelValue.advanced">
<v-row class="mb-6">
<v-col cols="12" md="4" sm="6">
<v-text-field
v-model="newStudentName"
class="mb-4"
hide-details
label="添加学生"
placeholder="输入学生姓名后回车添加"
prepend-inner-icon="mdi-account-plus"
variant="outlined"
@keyup.enter="addStudent"
>
<template #append>
<v-btn
:disabled="!newStudentName.trim()"
color="primary"
icon="mdi-plus"
variant="text"
@click="addStudent"
/>
</template>
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col
v-for="(student, index) in modelValue.list"
:key="index"
cols="12"
lg="3"
md="4"
sm="6"
>
<v-hover v-slot="{ isHovering, props }">
<v-card
:elevation="isMobile ? 1 : isHovering ? 4 : 1"
border
class="student-card"
v-bind="props"
>
<v-card-text class="d-flex align-center pa-3">
<v-menu :open-on-hover="!isMobile" location="bottom">
<template #activator="{ props: menuProps }">
<v-btn
class="mr-3 font-weight-medium"
size="small"
v-bind="menuProps"
variant="tonal"
>
{{ index + 1 }}
</v-btn>
</template>
<v-list density="compact" nav>
<v-list-item
:disabled="index === 0"
prepend-icon="mdi-arrow-up-bold"
@click="moveStudent(index, 'top')"
>
置顶
</v-list-item>
<v-divider/>
<v-list-item
:disabled="index === 0"
prepend-icon="mdi-arrow-up"
@click="moveStudent(index, 'up')"
>
上移
</v-list-item>
<v-list-item
:disabled="index === modelValue.list.length - 1"
prepend-icon="mdi-arrow-down"
@click="moveStudent(index, 'down')"
>
下移
</v-list-item>
</v-list>
</v-menu>
<v-text-field
v-if="editState.index === index"
v-model="editState.name"
autofocus
class="flex-grow-1"
density="compact"
hide-details
variant="underlined"
@blur="saveEdit"
@keyup.enter="saveEdit"
/>
<span
v-else
class="text-body-1 flex-grow-1"
@click="handleClick(index, student)"
>
{{ student.name }}
</span>
<div
:class="{ 'opacity-100': isHovering || isMobile }"
class="d-flex gap-1 action-buttons"
>
<v-btn
color="primary"
icon="mdi-pencil"
size="small"
variant="text"
@click="startEdit(index, student)"
/>
<v-btn
color="error"
icon="mdi-delete"
size="small"
variant="text"
@click="removeStudent(index)"
/>
</div>
</v-card-text>
</v-card>
</v-hover>
</v-col>
</v-row>
</div>
<!-- 高级编辑模式 -->
<div v-else class="pt-2">
<v-textarea
v-model="modelValue.text"
hint="使用文本编辑模式批量编辑学生名单,保存时会自动去除空行"
label="批量编辑学生列表"
persistent-hint
placeholder="每行输入一个学生姓名"
rows="10"
variant="outlined"
@update:model-value="handleTextInput"
/>
</div>
</v-expand-transition>
<v-row class="mt-6">
<v-col class="d-flex gap-2" cols="12">
<v-btn
:disabled="loading"
:loading="loading"
color="primary"
prepend-icon="mdi-content-save"
size="large"
@click="saveStudents"
>
保存名单
</v-btn>
<v-btn
:disabled="loading"
:loading="loading"
color="error"
prepend-icon="mdi-refresh"
size="large"
variant="outlined"
@click="loadStudents"
>
重载名单
</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import UnsavedWarning from "../common/UnsavedWarning.vue";
import "@/styles/warnings.scss";
import {pinyin} from "pinyin-pro";
import dataProvider from "@/utils/dataProvider";
import {getSetting} from "@/utils/settings";
export default {
name: "StudentListCard",
components: {
UnsavedWarning,
},
props: {
isMobile: Boolean,
},
data() {
return {
newStudentName: "",
editState: {
index: -1,
name: "",
},
modelValue: {
list: [],
text: "",
advanced: false,
},
loading: false,
error: null,
lastSavedData: null,
unsavedChanges: false,
};
},
watch: {
modelValue: {
handler(newData) {
if (this.lastSavedData) {
this.unsavedChanges = JSON.stringify(newData.list) !== JSON.stringify(this.lastSavedData);
}
if (!this.modelValue.advanced) {
this.modelValue.text = newData.list
.slice()
.sort((a, b) => a.id - b.id)
.map(s => s.name)
.join("\n");
}
},
deep: true,
},
},
mounted() {
this.loadStudents();
},
methods: {
async loadStudents() {
this.error = null;
try {
this.loading = true;
const classNum = getSetting("server.classNumber");
if (!classNum) {
throw new Error("请先设置班号");
}
try {
const response = await dataProvider.loadData("classworks-list-main");
if (response.success != false && Array.isArray(response)) {
this.modelValue.list = response.map((item, index) => {
if (typeof item === 'string') {
return {id: index + 1, name: item};
}
return {
id: item.id || index + 1,
name: item.name || item.toString()
};
});
this.modelValue.list.sort((a, b) => a.id - b.id);
this.modelValue.text = this.modelValue.list.map(s => s.name).join("\n");
this.lastSavedData = JSON.parse(JSON.stringify(this.modelValue.list));
this.unsavedChanges = false;
}
} catch (error) {
console.warn(
"Failed to load student list from dedicated key, falling back to config",
error
);
}
} catch (error) {
console.error("加载学生列表失败:", error);
this.error = error.message || "加载失败,请检查设置";
this.$message?.error("加载失败", this.error);
} finally {
this.loading = false;
}
},
async saveStudents() {
try {
const classNum = getSetting("server.classNumber");
if (!classNum) {
throw new Error("请先设置班号");
}
const formattedList = this.modelValue.list
.slice()
.sort((a, b) => a.id - b.id)
.map((student, index) => ({
id: index + 1,
name: student.name
}));
const response = await dataProvider.saveData(
"classworks-list-main",
formattedList
);
if (response.success === false) {
throw new Error(response.error?.message || "保存失败");
}
this.modelValue.list = formattedList;
this.lastSavedData = JSON.parse(JSON.stringify(formattedList));
this.unsavedChanges = false;
this.$message?.success("保存成功", "学生列表已更新");
} catch (error) {
console.error("保存学生列表失败:", error);
this.$message?.error("保存失败", error.message || "请重试");
}
},
toggleAdvanced() {
this.modelValue.advanced = !this.modelValue.advanced;
},
handleTextInput(text) {
if (!this.modelValue.advanced) return;
// Split the text into lines and filter out empty lines
const lines = text.split("\n").filter((line) => line.trim());
// Create a map of existing student names to their IDs
const currentIds = new Map(this.modelValue.list.map(s => [s.name, s.id]));
let maxId = Math.max(0, ...this.modelValue.list.map(s => s.id));
// Create new list preserving IDs for existing names and generating new IDs for new names
const newList = lines.map(name => {
name = name.trim();
if (currentIds.has(name)) {
return {id: currentIds.get(name), name};
}
return {id: ++maxId, name};
});
// Update the list
this.modelValue.list = newList;
},
addStudent() {
const name = this.newStudentName.trim();
if (name && !this.modelValue.list.some(s => s.name === name)) {
const maxId = Math.max(0, ...this.modelValue.list.map(s => s.id));
this.modelValue.list.push({id: maxId + 1, name});
this.newStudentName = "";
}
},
startEdit(index, student) {
this.editState.index = index;
this.editState.name = student.name;
},
saveEdit() {
if (this.editState.index !== -1) {
const newName = this.editState.name.trim();
if (newName && newName !== this.modelValue.list[this.editState.index].name) {
this.modelValue.list[this.editState.index].name = newName;
}
this.editState.index = -1;
this.editState.name = "";
}
},
removeStudent(index) {
if (index !== undefined) {
this.modelValue.list.splice(index, 1);
}
},
moveStudent(index, direction) {
if (direction === "top") {
if (index > 0) {
const student = this.modelValue.list[index];
this.modelValue.list.splice(index, 1);
this.modelValue.list.unshift(student);
this.modelValue.list.forEach((s, i) => s.id = i + 1);
}
} else {
const newIndex = direction === "up" ? index - 1 : index + 1;
if (newIndex >= 0 && newIndex < this.modelValue.list.length) {
[this.modelValue.list[index], this.modelValue.list[newIndex]] = [
this.modelValue.list[newIndex],
this.modelValue.list[index],
];
[this.modelValue.list[index].id, this.modelValue.list[newIndex].id] = [
this.modelValue.list[newIndex].id,
this.modelValue.list[index].id,
];
}
}
},
handleClick(index, student) {
if (this.isMobile) {
this.startEdit(index, student);
}
},
sortStudentsByPinyin() {
const sorted = [...this.modelValue.list].sort((a, b) => {
const pinyinA = pinyin(a.name, {toneType: "none"});
const pinyinB = pinyin(b.name, {toneType: "none"});
return pinyinA.localeCompare(pinyinB);
});
sorted.forEach((s, i) => s.id = i + 1);
this.modelValue.list = sorted;
},
},
};
</script>
<style lang="scss" scoped>
.student-card {
transition: all 0.2s ease;
}
.action-buttons {
opacity: 0;
transition: opacity 0.2s ease;
}
.unsaved-changes {
border-color: rgb(var(--v-theme-warning)) !important;
}
</style>