1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-07-02 00:59:23 +00:00

Refactor RandomPicker.vue for improved readability and maintainability. Enhance template formatting by standardizing indentation and spacing, and update button properties for consistency. Add a new button for navigating to the list page in index.vue, and adjust date handling in settings and index pages for better date management. Update dataProvider and settings utility functions for improved response handling and configuration management.

This commit is contained in:
SunWuyuan 2025-05-11 16:23:41 +08:00
parent 3654e22fef
commit be3ffb945c
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
8 changed files with 1060 additions and 124 deletions

View File

@ -1,6 +1,11 @@
<template>
<v-dialog v-model="dialog" max-width="600" fullscreen-breakpoint="sm" persistent>
<v-card class="random-picker-card">
<v-dialog
v-model="dialog"
max-width="600"
fullscreen-breakpoint="sm"
persistent
>
<v-card class="random-picker-card" rounded="xl" border>
<v-card-title class="text-h5 d-flex align-center">
<v-icon icon="mdi-account-question" class="mr-2" />
随机点名
@ -12,23 +17,41 @@
<div class="text-h6 mb-4">请选择抽取人数</div>
<div class="d-flex justify-center align-center counter-container">
<v-btn size="x-large" icon="mdi-minus" variant="tonal" color="primary" :disabled="count <= 1"
@click="decrementCount" class="counter-btn" />
<v-btn
size="x-large"
icon="mdi-minus"
variant="tonal"
color="primary"
:disabled="count <= 1"
@click="decrementCount"
class="counter-btn"
/>
<div class="count-display mx-8">
<span class="text-h2 font-weight-bold">{{ count }}</span>
<span class="text-subtitle-1 ml-2"></span>
</div>
<v-btn size="x-large" icon="mdi-plus" variant="tonal" color="primary" :disabled="count >= maxAllowedCount"
@click="incrementCount" class="counter-btn" />
<v-btn
size="x-large"
icon="mdi-plus"
variant="tonal"
color="primary"
:disabled="count >= maxAllowedCount"
@click="incrementCount"
class="counter-btn"
/>
</div>
<div class="mt-4">
<v-btn size="x-large" color="primary" prepend-icon="mdi-dice-multiple" @click="startPicking"
:disabled="filteredStudents.length === 0" class="start-btn">
<v-btn
size="x-large"
color="primary"
prepend-icon="mdi-dice-multiple"
@click="startPicking"
:disabled="filteredStudents.length === 0"
class="start-btn"
>
开始抽取
</v-btn>
</div>
@ -41,47 +64,75 @@
当前可抽取学生: {{ filteredStudents.length }}
<v-tooltip location="bottom">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" icon="mdi-information-outline" size="small" class="ml-1" />
<v-icon
v-bind="props"
icon="mdi-information-outline"
size="small"
class="ml-1"
/>
</template>
<div class="pa-2">
<div v-if="tempFilters.excludeAbsent"> 已排除请假学生 ({{ absentCount }})</div>
<div v-if="tempFilters.excludeLate"> 已排除迟到学生 ({{ lateCount }})</div>
<div v-if="tempFilters.excludeExcluded"> 已排除不参与学生 ({{ excludedCount }})</div>
</div>
</v-tooltip><!-- 添加临时过滤选项 -->
<div v-if="tempFilters.excludeAbsent">
已排除请假学生 ({{ absentCount }})
</div>
<div v-if="tempFilters.excludeLate">
已排除迟到学生 ({{ lateCount }})
</div>
<div v-if="tempFilters.excludeExcluded">
已排除不参与学生 ({{ excludedCount }})
</div>
</div> </v-tooltip
><!-- 添加临时过滤选项 -->
<div class="d-flex flex-wrap justify-center gap-2 mt-4">
<v-chip :color="tempFilters.excludeLate ? 'warning' : 'default'"
:variant="tempFilters.excludeLate ? 'elevated' : 'text'"
@click="tempFilters.excludeLate = !tempFilters.excludeLate" prepend-icon="mdi-clock-alert"
class="filter-chip">
{{ tempFilters.excludeLate ? '排除' : '包含' }}迟到学生
</v-chip>
<v-chip :color="tempFilters.excludeAbsent ? 'error' : 'default'"
:variant="tempFilters.excludeAbsent ? 'elevated' : 'text'"
@click="tempFilters.excludeAbsent = !tempFilters.excludeAbsent" prepend-icon="mdi-account-off"
class="filter-chip">
{{ tempFilters.excludeAbsent ? '排除' : '包含' }}请假学生
</v-chip>
<div class="d-flex flex-wrap justify-center gap-2 mt-4">
<v-chip
:color="tempFilters.excludeLate ? 'warning' : 'default'"
:variant="tempFilters.excludeLate ? 'elevated' : 'text'"
@click="tempFilters.excludeLate = !tempFilters.excludeLate"
prepend-icon="mdi-clock-alert"
class="filter-chip"
>
{{ tempFilters.excludeLate ? "排除" : "包含" }}迟到学生
</v-chip>
<v-chip
:color="tempFilters.excludeAbsent ? 'error' : 'default'"
:variant="tempFilters.excludeAbsent ? 'elevated' : 'text'"
@click="tempFilters.excludeAbsent = !tempFilters.excludeAbsent"
prepend-icon="mdi-account-off"
class="filter-chip"
>
{{ tempFilters.excludeAbsent ? "排除" : "包含" }}请假学生
</v-chip>
<v-chip :color="tempFilters.excludeExcluded ? 'grey' : 'default'"
:variant="tempFilters.excludeExcluded ? 'elevated' : 'text'"
@click="tempFilters.excludeExcluded = !tempFilters.excludeExcluded" prepend-icon="mdi-account-cancel"
class="filter-chip">
{{ tempFilters.excludeExcluded ? '排除' : '包含' }}不参与学生
</v-chip>
</div>
<v-chip
:color="tempFilters.excludeExcluded ? 'grey' : 'default'"
:variant="tempFilters.excludeExcluded ? 'elevated' : 'text'"
@click="
tempFilters.excludeExcluded = !tempFilters.excludeExcluded
"
prepend-icon="mdi-account-cancel"
class="filter-chip"
>
{{ tempFilters.excludeExcluded ? "排除" : "包含" }}不参与学生
</v-chip>
</div>
</div>
</v-card-text>
<v-card-text v-else class="text-center py-6">
<div v-if="isAnimating" class="animation-container">
<div class="animation-wrapper">
<transition-group name="shuffle" tag="div" class="shuffle-container">
<div v-for="(student, index) in animationStudents" :key="student.id" class="student-item"
:class="{ 'highlighted': highlightedIndices.includes(index) }">
<transition-group
name="shuffle"
tag="div"
class="shuffle-container"
>
<div
v-for="(student, index) in animationStudents"
:key="student.id"
class="student-item"
:class="{ highlighted: highlightedIndices.includes(index) }"
>
{{ student.name }}
</div>
</transition-group>
@ -90,21 +141,50 @@
<div v-else class="result-container">
<div class="text-h6 mb-4">抽取结果</div>
<v-card v-for="(student, index) in pickedStudents" :key="index" variant="outlined" color="primary"
class="mb-2 result-card">
<v-card-text class="text-h4 text-center py-4 d-flex align-center justify-center">
<v-card
v-for="(student, index) in pickedStudents"
:key="index"
variant="outlined"
color="primary"
class="mb-2 result-card"
>
<v-card-text
class="text-h4 text-center py-4 d-flex align-center justify-center"
>
{{ student }}
<v-btn icon="mdi-refresh" variant="text" size="small" class="ml-2 refresh-btn"
@click="refreshSingleStudent(index)" :disabled="remainingStudents.length === 0"
:title="remainingStudents.length === 0 ? '没有更多可用学生' : '重新抽取此学生'" />
<v-btn
icon="mdi-refresh"
variant="text"
size="small"
class="ml-2 refresh-btn"
@click="refreshSingleStudent(index)"
:disabled="remainingStudents.length === 0"
:title="
remainingStudents.length === 0
? '没有更多可用学生'
: '重新抽取此学生'
"
/>
</v-card-text>
</v-card>
<div class="mt-8 d-flex justify-center">
<v-btn color="primary" prepend-icon="mdi-refresh" @click="resetPicker" size="large" class="mx-2">
<v-btn
color="primary"
prepend-icon="mdi-refresh"
@click="resetPicker"
size="large"
class="mx-2"
>
重新抽取
</v-btn>
<v-btn color="grey" variant="outlined" @click="dialog = false" size="large" class="mx-2">
<v-btn
color="grey"
variant="outlined"
@click="dialog = false"
size="large"
class="mx-2"
>
关闭
</v-btn>
</div>
@ -115,25 +195,25 @@
</template>
<script>
import { getSetting } from '@/utils/settings';
import { getSetting } from "@/utils/settings";
export default {
name: 'RandomPicker',
name: "RandomPicker",
props: {
studentList: {
type: Array,
required: true
required: true,
},
attendance: {
type: Object,
required: true,
default: () => ({ absent: [], late: [], exclude: [] })
}
default: () => ({ absent: [], late: [], exclude: [] }),
},
},
data() {
return {
dialog: false,
count: getSetting('randomPicker.defaultCount'),
count: getSetting("randomPicker.defaultCount"),
isPickingStarted: false,
isAnimating: false,
pickedStudents: [],
@ -143,10 +223,10 @@ export default {
getSetting,
//
tempFilters: {
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
excludeLate: getSetting('randomPicker.excludeLate'),
excludeExcluded: getSetting('randomPicker.excludeExcluded')
}
excludeAbsent: getSetting("randomPicker.excludeAbsent"),
excludeLate: getSetting("randomPicker.excludeLate"),
excludeExcluded: getSetting("randomPicker.excludeExcluded"),
},
};
},
computed: {
@ -165,15 +245,24 @@ export default {
filteredStudents() {
if (!this.studentList || !this.studentList.length) return [];
return this.studentList.filter(student => {
return this.studentList.filter((student) => {
//
if (this.tempFilters.excludeAbsent && this.attendance.absent.includes(student)) {
if (
this.tempFilters.excludeAbsent &&
this.attendance.absent.includes(student)
) {
return false;
}
if (this.tempFilters.excludeLate && this.attendance.late.includes(student)) {
if (
this.tempFilters.excludeLate &&
this.attendance.late.includes(student)
) {
return false;
}
if (this.tempFilters.excludeExcluded && this.attendance.exclude.includes(student)) {
if (
this.tempFilters.excludeExcluded &&
this.attendance.exclude.includes(student)
) {
return false;
}
return true;
@ -191,23 +280,25 @@ export default {
//
remainingStudents() {
return this.filteredStudents.filter(student => !this.pickedStudents.includes(student));
}
return this.filteredStudents.filter(
(student) => !this.pickedStudents.includes(student)
);
},
},
watch: {
dialog(newVal) {
if (newVal) {
//
this.count = getSetting('randomPicker.defaultCount');
this.count = getSetting("randomPicker.defaultCount");
this.isPickingStarted = false;
this.isAnimating = false;
this.pickedStudents = [];
//
this.tempFilters = {
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
excludeLate: getSetting('randomPicker.excludeLate'),
excludeExcluded: getSetting('randomPicker.excludeExcluded')
excludeAbsent: getSetting("randomPicker.excludeAbsent"),
excludeLate: getSetting("randomPicker.excludeLate"),
excludeExcluded: getSetting("randomPicker.excludeExcluded"),
};
} else {
//
@ -219,14 +310,14 @@ export default {
},
// count
'tempFilters': {
tempFilters: {
handler() {
if (this.count > this.maxAllowedCount) {
this.count = Math.max(1, this.maxAllowedCount);
}
},
deep: true
}
deep: true,
},
},
methods: {
open() {
@ -247,7 +338,7 @@ export default {
this.isPickingStarted = true;
if (getSetting('randomPicker.animation')) {
if (getSetting("randomPicker.animation")) {
this.startAnimation();
} else {
this.finishPicking();
@ -259,7 +350,7 @@ export default {
// ID便
this.animationStudents = this.filteredStudents.map((name, index) => ({
id: `student-${index}`,
name
name,
}));
//
@ -279,7 +370,9 @@ export default {
for (let i = 0; i < this.count; i++) {
let randomIndex;
do {
randomIndex = Math.floor(Math.random() * this.animationStudents.length);
randomIndex = Math.floor(
Math.random() * this.animationStudents.length
);
} while (indices.includes(randomIndex));
indices.push(randomIndex);
}
@ -289,7 +382,7 @@ export default {
currentStep++;
// 使
const nextInterval = intervalTime + (currentStep * 20);
const nextInterval = intervalTime + currentStep * 20;
if (currentStep < totalSteps) {
this.animationTimer = setTimeout(animate, nextInterval);
@ -308,7 +401,9 @@ export default {
this.isAnimating = false;
//
const shuffled = [...this.filteredStudents].sort(() => 0.5 - Math.random());
const shuffled = [...this.filteredStudents].sort(
() => 0.5 - Math.random()
);
this.pickedStudents = shuffled.slice(0, this.count);
},
resetPicker() {
@ -325,23 +420,25 @@ export default {
if (this.remainingStudents.length === 0) return;
//
const randomIndex = Math.floor(Math.random() * this.remainingStudents.length);
const randomIndex = Math.floor(
Math.random() * this.remainingStudents.length
);
const newStudent = this.remainingStudents[randomIndex];
//
this.pickedStudents[index] = newStudent;
//
const resultCards = document.querySelectorAll('.result-card');
const resultCards = document.querySelectorAll(".result-card");
if (resultCards[index]) {
resultCards[index].classList.add('refresh-animation');
resultCards[index].classList.add("refresh-animation");
setTimeout(() => {
resultCards[index].classList.remove('refresh-animation');
resultCards[index].classList.remove("refresh-animation");
}, 500);
}
}
}
}
},
},
};
</script>
<style lang="scss" scoped>
@ -498,7 +595,6 @@ export default {
//
@media (hover: none) {
.counter-btn,
.start-btn {
min-height: 72px;
@ -520,4 +616,4 @@ export default {
font-size: 1rem;
}
}
</style>
</style>

View File

@ -149,6 +149,16 @@
>
随机点名
</v-btn>
<v-btn
v-if="showListCardButton"
color="primary-darken-1"
prepend-icon="mdi-list-box"
size="large"
class="ml-2"
@click="$router.push('/list')"
>
列表
</v-btn>
<v-btn
v-if="showFullscreenButton"
:color="state.isFullscreen ? 'blue-grey' : 'blue'"
@ -673,8 +683,8 @@ export default {
snackbarText: "",
fontSize: getSetting("font.size"),
datePickerDialog: false,
selectedDate: new Date().toISOString().split("T")[0],
selectedDateObj: new Date(this.selectedDate),
selectedDate: new Date().toISOString().split("T")[0].replace(/-/g, ''),
selectedDateObj: new Date(),
refreshInterval: null,
subjectOrder: [
"语文",
@ -849,6 +859,9 @@ export default {
showRandomPickerButton() {
return getSetting("randomPicker.enabled");
},
showListCardButton() {
return getSetting("display.showListCard");
},
confirmNonTodaySave() {
return getSetting("edit.confirmNonTodaySave");
},
@ -1066,9 +1079,26 @@ export default {
const today = this.getToday();
//
const currentDate = dateFromUrl ? new Date(dateFromUrl) : today;
let currentDate = today;
if (dateFromUrl) {
// yyyymmdd
if (/^\d{8}$/.test(dateFromUrl)) {
const year = dateFromUrl.substring(0, 4);
const month = dateFromUrl.substring(4, 6);
const day = dateFromUrl.substring(6, 8);
currentDate = new Date(`${year}-${month}-${day}`);
} else {
currentDate = new Date(dateFromUrl);
}
// 使
if (isNaN(currentDate.getTime())) {
currentDate = today;
}
}
this.state.dateString = this.formatDate(currentDate);
this.state.selectedDate = this.state.dateString;
this.state.selectedDateObj = currentDate; //
this.state.isToday =
this.formatDate(currentDate) === this.formatDate(today);
// URL使
@ -1090,7 +1120,7 @@ export default {
"classworks-data-" + this.state.dateString
);
if (!response.success) {
if (response.success == false) {
if (response.error.code === "NOT_FOUND") {
this.state.showNoDataMessage = true;
this.state.noDataMessage = response.error.message;
@ -1105,11 +1135,11 @@ export default {
} else {
//
this.state.boardData = {
homework: response.data.homework || {},
homework: response.homework || {},
attendance: {
absent: response.data.attendance?.absent || [],
late: response.data.attendance?.late || [],
exclude: response.data.attendance?.exclude || [],
absent: response.attendance?.absent || [],
late: response.attendance?.late || [],
exclude: response.attendance?.exclude || [],
},
};
this.state.synced = true;
@ -1194,8 +1224,7 @@ export default {
"classworks-data-" + this.state.dateString,
this.state.boardData
);
if (!response.success) {
if (response.success == false) {
throw new Error(response.error.message);
}
@ -1212,9 +1241,9 @@ export default {
// Try to get student list from the dedicated key
const response = await dataProvider.loadData("classworks-list-main");
if (response.success && Array.isArray(response.data)) {
if (response.success != false && Array.isArray(response)) {
// Transform the data into a simple list of names
this.state.studentList = response.data.map(
this.state.studentList = response.map(
(student) => student.name
);
return;
@ -1390,6 +1419,7 @@ export default {
if (this.state.dateString !== formattedDate) {
this.state.dateString = formattedDate;
this.state.selectedDate = formattedDate;
this.state.selectedDateObj = selectedDate;
this.state.isToday =
formattedDate === this.formatDate(this.getToday());

581
src/pages/list/[id].vue Normal file
View File

@ -0,0 +1,581 @@
<template><v-app-bar elevation="1">
<template #prepend>
<v-btn
icon="mdi-arrow-left"
variant="text"
@click="$router.push('/')"
/>
</template>
<v-app-bar-title class="text-h6" v-if="list && !isRenaming">{{ list.name }}</v-app-bar-title>
<v-app-bar-title class="text-h6" v-else>列表</v-app-bar-title>
</v-app-bar>
<v-container>
<div class="d-flex align-center mb-4">
<v-btn
icon
class="mr-2"
to="/list"
border
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<h1 v-if="list && !isRenaming">
{{ list.name }}
<v-btn icon size="small" @click="startRenaming" border>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</h1>
<div v-else-if="list && isRenaming" class="d-flex align-center">
<v-text-field
v-model="newListName"
label="列表名称"
hide-details
density="compact"
class="mr-2"
style="min-width: 200px;"
autofocus
@keyup.enter="saveListName"
></v-text-field>
<v-btn color="primary" size="small" class="mr-2" @click="saveListName">
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn color="error" size="small" @click="cancelRenaming">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<h1 v-else>
加载中...
</h1>
</div>
<v-card class="mb-5" border rounded="xl">
<v-card-title class="d-flex align-center">
项目列表
<v-spacer />
<v-btn-toggle
v-model="sortType"
mandatory
>
<v-btn value="default">
<v-icon>mdi-sort-alphabetical-ascending</v-icon>
</v-btn>
<v-btn value="completed">
<v-icon>mdi-check-circle-outline</v-icon>
</v-btn>
</v-btn-toggle>
</v-card-title>
<v-card-text v-if="sortedItems.length === 0">
暂无项目请添加新项目
</v-card-text>
<v-list
v-else
select-strategy="leaf"
>
<v-list-item
v-for="(item, index) in sortedItems"
:key="item.id"
:class="{ 'text-decoration-line-through': item.completed }"
@click="openItemDetails(item)"
>
<template #prepend>
<v-list-item-action start>
<v-checkbox-btn
:model-value="item.completed"
@update:model-value="updateItemStatus(item.id, $event)"
@click.stop
/>
</v-list-item-action>
</template>
{{ item.name }}
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
<template #append>
{{ index + 1 }}
</template>
</v-list-item>
</v-list>
<v-card-actions v-if="sortedItems.length > 0">
<v-spacer />
<v-btn
color="error"
prepend-icon="mdi-delete-sweep"
@click="confirmDeleteCompleted"
:disabled="!hasCompletedItems"
>
删除已完成项目
</v-btn>
</v-card-actions>
</v-card><v-card class="mb-5" border rounded="xl">
<v-card-title>添加新项目</v-card-title>
<v-card-text>
<v-text-field
v-model="newItemName"
label="项目名称"
:rules="[(v) => !!v || '名称不能为空']"
/>
<v-btn
color="primary"
:disabled="!newItemName"
@click="addItem"
>
添加
</v-btn>
</v-card-text>
</v-card>
<v-card class="mb-5" border rounded="xl">
<v-card-title>列表排序</v-card-title>
<v-card-text>
<v-text-field
v-model="sortSeed"
label="排序种子 (任意数字或文本)"
hint="输入相同的种子值可以得到相同的排序结果"
persistent-hint
class="mb-3"
/>
<v-btn
color="primary"
class="mr-2"
@click="randomSort"
>
随机排序
</v-btn>
<v-btn
variant="text"
@click="resetSort"
>
撤销
</v-btn>
</v-card-text>
</v-card>
<!-- 确认删除对话框 -->
<v-dialog v-model="deleteDialog.show" max-width="500">
<v-card border rounded="xl">
<v-card-title>{{ deleteDialog.title }}</v-card-title>
<v-card-text>{{ deleteDialog.text }}</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="deleteDialog.show = false">
取消
</v-btn>
<v-btn color="error" variant="text" @click="confirmDelete">
确认删除
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 项目详情对话框 -->
<v-dialog v-model="itemDialog.show" max-width="600">
<v-card border rounded="xl">
<v-card-title>
<span v-if="!itemDialog.isEditing">项目详情</span>
<span v-else>编辑项目</span>
</v-card-title>
<v-card-text>
<div v-if="!itemDialog.isEditing && itemDialog.item">
<v-list>
<v-list-item>
<v-list-item-title class="text-subtitle-1 font-weight-bold">{{ itemDialog.item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ itemDialog.item.id }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title class="text-subtitle-1 font-weight-bold">状态</v-list-item-title>
<v-list-item-subtitle>
<v-chip
:color="itemDialog.item.completed ? 'success' : 'warning'"
size="small"
>
{{ itemDialog.item.completed ? '已完成' : '未完成' }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="itemDialog.item.description">
<v-list-item-title class="text-subtitle-1 font-weight-bold">描述</v-list-item-title>
<v-list-item-subtitle>{{ itemDialog.item.description }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</div>
<div v-else-if="itemDialog.isEditing && itemDialog.item" class="pa-2">
<v-text-field
v-model="itemDialog.editedItem.name"
label="名称"
variant="outlined"
class="mb-3"
></v-text-field>
<v-textarea
v-model="itemDialog.editedItem.description"
label="描述"
variant="outlined"
rows="3"
class="mb-3"
></v-textarea>
<v-switch
v-model="itemDialog.editedItem.completed"
label="已完成"
color="success"
hide-details
></v-switch>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<template v-if="!itemDialog.isEditing">
<v-btn color="primary" variant="text" @click="startEditingItem">
编辑
</v-btn>
<v-btn color="error" variant="text" @click="confirmDeleteItem(itemDialog.item?.id)">
删除
</v-btn>
<v-btn color="secondary" variant="text" @click="itemDialog.show = false">
关闭
</v-btn>
</template>
<template v-else>
<v-btn color="success" variant="text" @click="saveItemChanges">
保存
</v-btn>
<v-btn color="secondary" variant="text" @click="cancelEditingItem">
取消
</v-btn>
</template>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import dataProvider from "@/utils/dataProvider.js";
export default {
data() {
return {
listId: null,
list: null,
items: [],
originalItems: [], //
newItemName: "",
sortSeed: "1", //
sortType: "default", //
isRandomSorted: false, //
deleteDialog: {
show: false,
title: "",
text: "",
itemId: null,
action: null
},
isRenaming: false,
newListName: "",
itemDialog: {
show: false,
item: null,
isEditing: false,
editedItem: null
}
};
},
computed: {
sortedItems() {
if (this.sortType === "completed") {
//
return [...this.items].sort((a, b) => {
if (a.completed === b.completed) return 0;
return a.completed ? 1 : -1;
});
}
//
return this.items;
},
hasCompletedItems() {
return this.items.some(item => item.completed);
}
},
async created() {
this.listId = this.$route.params.id;
await Promise.all([this.loadListInfo(), this.loadItems()]);
},
methods: {
async loadListInfo() {
try {
const listsInfo = await dataProvider.loadData("classworks-list-info");
if (listsInfo && Array.isArray(listsInfo)) {
this.list = listsInfo.find((list) => list.id === this.listId);
}
if (!this.list) {
// List not found, redirect back to list page
this.$router.push("/list");
}
} catch (error) {
console.error("Failed to load list info", error);
this.$router.push("/list");
}
},
startRenaming() {
if (this.list) {
this.newListName = this.list.name;
this.isRenaming = true;
}
},
cancelRenaming() {
this.isRenaming = false;
this.newListName = "";
},
async saveListName() {
if (!this.newListName.trim()) {
return;
}
try {
//
const listsInfo = await dataProvider.loadData("classworks-list-info");
if (listsInfo && Array.isArray(listsInfo)) {
//
const listIndex = listsInfo.findIndex((list) => list.id === this.listId);
if (listIndex !== -1) {
listsInfo[listIndex].name = this.newListName.trim();
//
await dataProvider.saveData("classworks-list-info", listsInfo);
//
this.list.name = this.newListName.trim();
}
}
// 退
this.isRenaming = false;
} catch (error) {
console.error("Failed to update list name", error);
}
},
async loadItems() {
try {
let items = await dataProvider.loadData(
`classworks-list-${this.listId}`
);
if (!items || !Array.isArray(items)) {
items = [];
await dataProvider.saveData(`classworks-list-${this.listId}`, items);
}
//
this.items = items.map(item => {
// completed
if (typeof item.completed === 'undefined') {
return {
id: item.id || Date.now() + Math.floor(Math.random() * 1000),
name: item.name,
completed: false,
description: item.description || '',
};
}
//
return {
...item,
description: item.description || '',
};
});
//
this.originalItems = JSON.parse(JSON.stringify(this.items));
} catch (error) {
console.error("Failed to load items", error);
this.items = [];
this.originalItems = [];
}
},
async addItem() {
if (!this.newItemName) return;
const newItem = {
id: Date.now().toString(),
name: this.newItemName,
completed: false,
description: '',
};
this.items.push(newItem);
//
this.originalItems.push(JSON.parse(JSON.stringify(newItem)));
await this.saveItems();
this.newItemName = "";
},
openItemDetails(item) {
this.itemDialog = {
show: true,
item: item,
isEditing: false,
editedItem: null
};
},
startEditingItem() {
if (!this.itemDialog.item) return;
this.itemDialog.isEditing = true;
this.itemDialog.editedItem = JSON.parse(JSON.stringify(this.itemDialog.item));
},
cancelEditingItem() {
this.itemDialog.isEditing = false;
this.itemDialog.editedItem = null;
},
async saveItemChanges() {
if (!this.itemDialog.editedItem) return;
const itemIndex = this.items.findIndex(item => item.id === this.itemDialog.item.id);
if (itemIndex !== -1) {
//
this.items[itemIndex] = {
...this.itemDialog.editedItem
};
//
const originalIndex = this.originalItems.findIndex(item => item.id === this.itemDialog.item.id);
if (originalIndex !== -1) {
this.originalItems[originalIndex] = JSON.parse(JSON.stringify(this.items[itemIndex]));
}
await this.saveItems();
//
this.itemDialog.item = this.items[itemIndex];
this.itemDialog.isEditing = false;
this.itemDialog.editedItem = null;
}
},
confirmDeleteItem(itemId) {
const item = this.items.find(item => item.id === itemId);
if (item) {
this.deleteDialog = {
show: true,
title: "删除确认",
text: `确定要删除 "${item.name}" 吗?`,
itemId: itemId,
action: 'deleteItem'
};
//
if (this.itemDialog.show && this.itemDialog.item?.id === itemId) {
this.itemDialog.show = false;
}
}
},
confirmDeleteCompleted() {
const completedCount = this.items.filter(item => item.completed).length;
this.deleteDialog = {
show: true,
title: "删除已完成项目",
text: `确定要删除所有已完成的项目吗?(共 ${completedCount} 项)`,
action: 'deleteCompleted'
};
},
confirmDelete() {
if (this.deleteDialog.action === 'deleteItem' && this.deleteDialog.itemId) {
this.deleteItem(this.deleteDialog.itemId);
} else if (this.deleteDialog.action === 'deleteCompleted') {
this.deleteCompletedItems();
}
this.deleteDialog.show = false;
},
async deleteItem(itemId) {
this.items = this.items.filter((item) => item.id !== itemId);
this.originalItems = this.originalItems.filter(
(item) => item.id !== itemId
);
await this.saveItems();
},
async deleteCompletedItems() {
this.items = this.items.filter(item => !item.completed);
this.originalItems = this.originalItems.filter(item => !item.completed);
await this.saveItems();
},
async updateItemStatus(itemId, newStatus) {
//
const item = this.items.find(item => item.id === itemId);
if (item) {
item.completed = newStatus;
//
const originalItem = this.originalItems.find(item => item.id === itemId);
if (originalItem) {
originalItem.completed = newStatus;
}
await this.saveItems();
}
},
async saveItems() {
try {
await dataProvider.saveData(
`classworks-list-${this.listId}`,
this.items
);
} catch (error) {
console.error("Failed to save items", error);
}
},
// 使
randomSort() {
// ID
const itemsWithRandom = this.items.map((item) => {
//
const itemSeed = this.hashCode(item.id + this.sortSeed);
return {
...item,
randomValue: this.seededRandom(itemSeed),
};
});
//
itemsWithRandom.sort((a, b) => a.randomValue - b.randomValue);
// randomValueitems
this.items = itemsWithRandom.map((item) => {
const newItem = { ...item };
delete newItem.randomValue;
return newItem;
});
this.isRandomSorted = true;
this.saveItems(); //
},
//
resetSort() {
this.items = JSON.parse(JSON.stringify(this.originalItems));
this.isRandomSorted = false;
this.saveItems();
},
//
hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash); //
},
//
seededRandom(seed) {
// 使
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
},
},
};
</script>
<style scoped></style>

243
src/pages/list/index.vue Normal file
View File

@ -0,0 +1,243 @@
<template><v-app-bar elevation="1">
<template #prepend>
<v-btn
icon="mdi-arrow-left"
variant="text"
@click="$router.push('/')"
/>
</template>
<v-app-bar-title class="text-h6">列表</v-app-bar-title>
</v-app-bar><v-container>
<v-card border class="mb-5" rounded="xl">
<v-card-title>现有列表</v-card-title>
<v-card-text v-if="lists.length === 0">
暂无列表请创建新列表
</v-card-text>
<v-list v-else>
<v-list-item
v-for="list in lists"
:key="list.id"
:to="list.id !== editingListId ? `/list/${list.id}` : undefined"
:active="list.id === editingListId"
>
<div v-if="list.id !== editingListId">
<v-list-item-title>{{ list.name }}</v-list-item-title>
</div>
<div v-else class="d-flex align-center w-100">
<v-text-field
v-model="editListName"
label="列表名称"
hide-details
density="compact"
class="mr-2"
autofocus
@keyup.enter="saveListName"
></v-text-field>
<v-btn icon color="primary" @click.stop.prevent="saveListName" class="mr-2" border>
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn icon color="error" @click.stop.prevent="cancelEditing" border>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<template #append>
<div v-if="list.id !== editingListId">
<v-btn icon @click.stop.prevent="startEditing(list.id)" class="mr-2" border>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon @click.stop.prevent="confirmDeleteList(list.id)" border>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<v-card class="mb-5" border rounded="xl">
<v-card-title>创建新列表</v-card-title>
<v-card-text>
<v-text-field
v-model="newListName"
label="列表名称"
:rules="[v => !!v || '名称不能为空']"
></v-text-field>
<v-btn color="primary" @click="createNewList" :disabled="!newListName">
创建列表
</v-btn>
</v-card-text>
</v-card>
<!-- 确认删除对话框 -->
<v-dialog v-model="deleteDialog.show" max-width="500">
<v-card border>
<v-card-title>删除列表</v-card-title>
<v-card-text>{{ deleteDialog.text }}</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="deleteDialog.show = false">
取消
</v-btn>
<v-btn color="error" variant="text" @click="confirmDelete">
确认删除
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import dataProvider from "@/utils/dataProvider.js";
export default {
data() {
return {
lists: [],
newListName: "",
studentList: [], //
deleteDialog: {
show: false,
text: "",
listId: null
},
editingListId: null,
editListName: ""
};
},
async created() {
await Promise.all([
this.loadLists(),
this.loadStudentList()
]);
},
methods: {
async loadLists() {
try {
let listsInfo = await dataProvider.loadData("classworks-list-info");
if (!listsInfo || !Array.isArray(listsInfo)) {
listsInfo = [];
await dataProvider.saveData("classworks-list-info", listsInfo);
}
this.lists = listsInfo;
} catch (error) {
console.error("Failed to load lists", error);
this.lists = [];
await dataProvider.saveData("classworks-list-info", []);
}
},
async loadStudentList() {
try {
// classworks-list-main
const response = await dataProvider.loadData("classworks-list-main");
if (response && Array.isArray(response)) {
this.studentList = response;
} else {
this.studentList = [];
}
} catch (error) {
console.error("Failed to load student list", error);
this.studentList = [];
}
},
async createNewList() {
if (!this.newListName) return;
const listId = Date.now().toString();
const newList = {
id: listId,
name: this.newListName,
};
// Add to lists info
this.lists.push(newList);
await dataProvider.saveData("classworks-list-info", this.lists);
//
const newListData = [];
//
if (this.studentList && this.studentList.length > 0) {
this.studentList.forEach(student => {
newListData.push({
id: student.id || Date.now() + Math.floor(Math.random() * 1000),
name: student.name,
completed: false,
});
});
}
await dataProvider.saveData(`classworks-list-${listId}`, newListData);
this.newListName = "";
// Navigate to the new list
this.$router.push(`/list/${listId}`);
},
startEditing(listId) {
const list = this.lists.find(list => list.id === listId);
if (list) {
this.editingListId = listId;
this.editListName = list.name;
}
},
cancelEditing() {
this.editingListId = null;
this.editListName = "";
},
async saveListName() {
if (!this.editListName.trim() || !this.editingListId) {
return;
}
try {
//
const listIndex = this.lists.findIndex(list => list.id === this.editingListId);
if (listIndex !== -1) {
this.lists[listIndex].name = this.editListName.trim();
//
await dataProvider.saveData("classworks-list-info", this.lists);
}
// 退
this.editingListId = null;
this.editListName = "";
} catch (error) {
console.error("Failed to update list name", error);
}
},
confirmDeleteList(listId) {
const list = this.lists.find(list => list.id === listId);
if (list) {
this.deleteDialog = {
show: true,
text: `确定要删除列表 "${list.name}" 吗?`,
listId: listId
};
}
},
confirmDelete() {
if (this.deleteDialog.listId) {
this.deleteList(this.deleteDialog.listId);
}
this.deleteDialog.show = false;
},
async deleteList(listId) {
this.lists = this.lists.filter(list => list.id !== listId);
await dataProvider.saveData("classworks-list-info", this.lists);
// We don't need to delete the actual list data as it will just remain unused
}
}
};
</script>
<style scoped></style>

View File

@ -383,9 +383,9 @@ export default {
// Try to get student list from the dedicated key
const response = await dataProvider.loadData('classworks-list-main');
if (response.success && Array.isArray(response.data)) {
if (response.success!=false && Array.isArray(response)) {
// Transform the data into a simple list of names
this.studentData.list = response.data.map(student => student.name);
this.studentData.list = response.map(student => student.name);
this.studentData.text = this.studentData.list.join('\n');
this.lastSavedData = [...this.studentData.list];
this.hasUnsavedChanges = false;
@ -394,21 +394,6 @@ export default {
} catch (error) {
console.warn('Failed to load student list from dedicated key, falling back to config', error);
}
// Fall back to retrieving from config if the dedicated key is not available
const response = await kvProvider.local.loadConfig();
if (response.success && response.data && Array.isArray(response.data.studentList)) {
this.studentData.list = response.data.studentList;
this.studentData.text = response.data.studentList.join('\n');
this.lastSavedData = [...response.data.studentList];
this.hasUnsavedChanges = false;
} else {
// If no student list is found anywhere, initialize with empty list
this.studentData.list = [];
this.studentData.text = '';
this.lastSavedData = [];
}
} catch (error) {
console.error('加载学生列表失败:', error);
this.studentsError = error.message || '加载失败,请检查设置';
@ -436,7 +421,7 @@ export default {
// Save the student list to the dedicated key
const response = await dataProvider.saveData("classworks-list-main", formattedStudentList);
if (!response.success) {
if (response.success==false) {
throw new Error(response.error?.message || "保存失败");
}

View File

@ -1,11 +1,7 @@
import { kvProvider } from "./providers/kvProvider";
import { getSetting } from "./settings";
export const formatResponse = (data, message = null) => ({
success: true,
data,
message,
});
export const formatResponse = (data, message = null) => (data);
export const formatError = (message, code = "UNKNOWN_ERROR") => ({
success: false,

View File

@ -62,7 +62,7 @@ export const kvProvider = {
const db = await initDB();
await db.put("kv", JSON.stringify(data), key);
return formatResponse(null, "保存成功");
return formatResponse(true, "保存成功");
} catch (error) {
return formatError("保存本地数据失败:" + error);
}
@ -100,7 +100,7 @@ export const kvProvider = {
await axios.post(`${serverUrl}/${machineId}/${key}`, data, {
headers: getHeaders(),
});
return formatResponse(null, "保存成功");
return formatResponse(true);
} catch (error) {
return formatError(
error.response?.data?.message || "保存失败",

View File

@ -146,7 +146,12 @@ const settingsDefinitions = {
description: "是否显示防烧屏忽悠卡片",
icon: "mdi-monitor-shimmer",
},
"display.showListCard": {
type: "boolean",
default: true,
description: "是否显示列表卡片",
icon: "mdi-list-box",
},
// 服务器设置(合并了数据提供者设置)
"server.domain": {
type: "string",