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
8 changed files with 1060 additions and 124 deletions
+581
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);
// 移除临时的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
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>