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:
parent
3654e22fef
commit
be3ffb945c
@ -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>
|
||||
|
@ -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
581
src/pages/list/[id].vue
Normal 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);
|
||||
|
||||
// 移除临时的randomValue属性并更新items
|
||||
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
243
src/pages/list/index.vue
Normal 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>
|
@ -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 || "保存失败");
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 || "保存失败",
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user