refactor: migrate frontend todo page

This commit is contained in:
2026-07-19 15:57:50 +08:00
parent 4c02b47620
commit 0402f2d2b7
5 changed files with 192 additions and 5 deletions
+80
View File
@@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref } from 'vue';
import { lineBreaker2Br } from '@/utils/utils';
const props = defineProps({
uuid: {
type: String,
required: true
},
data: {
type: String,
required: true
}
})
const emit = defineEmits<{
(e: 'delete', uuid: string): void
(e: 'update', uuid: string, data: string): void
}>()
const isEditing = ref<boolean>(false);
const editingData = ref<string>("");
const editItem = () => {
// copy current data to textarea
editingData.value = props.data;
// switch to edit mode
isEditing.value = true;
}
const deleteItem = () => {
emit('delete', props.uuid);
}
const updateItem = () => {
const new_data = editingData.value;
// clean data
editingData.value = "";
// switch to normal mode
isEditing.value = false;
emit('update', props.uuid, new_data);
}
const cancelUpdateItem = () => {
// clean data
editingData.value = "";
// switch to normal mode
isEditing.value = false;
}
</script>
<template>
<div class="paperbox-item card">
<div class="paperbox-item-words">
<p v-show="!isEditing" v-html="lineBreaker2Br(data)"></p>
<textarea v-show="isEditing" v-model="editingData" class="textarea"></textarea>
</div>
<div class="paperbox-item-icon control" v-show="!isEditing" @click="editItem">
<button class="button"><span class="icon is-small"><font-awesome-icon
icon="fas fa-pen"></font-awesome-icon></span></button>
</div>
<div class="paperbox-item-icon control" v-show="!isEditing" @click="deleteItem">
<button class="button"><span class="icon is-small"><font-awesome-icon
icon="fas fa-trash"></font-awesome-icon></span></button>
</div>
<div class="paperbox-item-icon control" v-show="isEditing" @click="updateItem">
<button class="button"><span class="icon is-small"><font-awesome-icon
icon="fas fa-check"></font-awesome-icon></span></button>
</div>
<div class="paperbox-item-icon control" v-show="isEditing" @click="cancelUpdateItem">
<button class="button"><span class="icon is-small"><font-awesome-icon
icon="fas fa-times"></font-awesome-icon></span></button>
</div>
</div>
</template>
<style scoped></style>
+15
View File
@@ -38,3 +38,18 @@ export function getWeekday(date: Date): number {
if (day == 0) return 6;
else return day - 1;
}
/**
* Escape HTML special characters in the string and convert newlines to `<br />`
* so it can be safely inserted via `v-html`. Port of the legacy jQuery-based
* `LineBreaker2Br` (`$('<div>').text(strl).html().replace(/\n/g, '<br />')`).
* @param strl The raw string to convert.
* @returns The escaped, newline-broken HTML string.
*/
export function lineBreaker2Br(strl: string): string {
return strl
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br />');
}
+1 -1
View File
@@ -78,7 +78,7 @@ const addOwnedItem = async () => {
const deleteOwnedItem = async (uuid: string) => {
const rv = await apiCollectionDeleteOwn(token.currentToken, uuid, ownedItems.value.get(uuid)![2]);
if (rv) {
if (!rv) {
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
return;
}
+96 -3
View File
@@ -1,8 +1,101 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useTokenStore } from '@/stores/token';
import MessageBox from '@/components/MessageBox.vue';
import {
getFull as apiTodoGetFull,
add as apiTodoAdd,
update as apiTodoUpdate,
del as apiTodoDel,
type TodoRow,
} from '@/api/todo';
import TodoItem from '@/components/todo/TodoItem.vue';
const todoItems = ref<Map<string, TodoRow>>(new Map());
const token = useTokenStore();
const messagebox = ref<InstanceType<typeof MessageBox>>();
onMounted(async () => {
await refreshTodoItem();
})
const refreshTodoItem = async () => {
// clean todo items
todoItems.value.clear();
// fetch data
const rv = await apiTodoGetFull(token.currentToken);
if (typeof rv === 'undefined') {
console.error("fail to fetch todo");
return;
}
// add into dict for rendering
for (const item of rv) {
todoItems.value.set(item[0], item);
}
}
const addTodoItem = async () => {
const rv = await apiTodoAdd(token.currentToken);
if (typeof rv === 'undefined') {
messagebox.value?.show("An add operation failed. It may caused by wrong arguments. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
return;
}
todoItems.value.set(rv[0], rv);
}
const deleteTodoItem = async (uuid: string) => {
const rv = await apiTodoDel(token.currentToken, uuid, todoItems.value.get(uuid)![3]);
if (!rv) {
messagebox.value?.show("A delete operation failed. It may caused by no matched item. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
return;
}
// remove from rendered list
todoItems.value.delete(uuid);
}
const updateTodoItem = async (uuid: string, data: string) => {
const lastChange = todoItems.value.get(uuid)![3];
const rv = await apiTodoUpdate(token.currentToken, uuid, data, lastChange);
if (typeof rv === 'undefined') {
messagebox.value?.show("An update operation failed. It may caused by wrong arguments or lost target. Refreshing page may fix system problem. Before refreshing page, please backup all your unsaved data.");
return;
}
// safely update data & lastChange
const row = todoItems.value.get(uuid)!;
row[2] = data;
row[3] = rv;
}
</script>
<template>
<h1>Congratulations</h1>
<p>This is todo.</p>
<div class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
<h1 class="title">Todo list</h1>
<div class="button-list">
<div class="control" @click="addTodoItem">
<a class="button is-primary">
<span class="icon is-small"><font-awesome-icon icon="fas fa-plus"></font-awesome-icon></span>
</a>
</div>
<div class="control" @click="refreshTodoItem">
<a class="button is-primary">
<span class="icon is-small"><font-awesome-icon icon="fas fa-sync"></font-awesome-icon></span>
</a>
</div>
</div>
<div v-for="item in todoItems.values()" :key="item[0]" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
<TodoItem :uuid="item[0]" :data="item[2]" @delete="deleteTodoItem" @update="updateTodoItem" />
</div>
</div>
<MessageBox ref="messagebox" />
</template>
<style scoped></style>