refactor: migrate frontend todo page
This commit is contained in:
@@ -17,7 +17,6 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
"@fortawesome/free-solid-svg-icons": "^7.2.0",
|
||||||
"@fortawesome/vue-fontawesome": "^3.2.0",
|
"@fortawesome/vue-fontawesome": "^3.2.0",
|
||||||
"axios": "1.14.0",
|
|
||||||
"bulma": "0.9.1",
|
"bulma": "0.9.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pinia-plugin-persistedstate": "^4.7.1",
|
"pinia-plugin-persistedstate": "^4.7.1",
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -38,3 +38,18 @@ export function getWeekday(date: Date): number {
|
|||||||
if (day == 0) return 6;
|
if (day == 0) return 6;
|
||||||
else return day - 1;
|
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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/\n/g, '<br />');
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const addOwnedItem = async () => {
|
|||||||
|
|
||||||
const deleteOwnedItem = async (uuid: string) => {
|
const deleteOwnedItem = async (uuid: string) => {
|
||||||
const rv = await apiCollectionDeleteOwn(token.currentToken, uuid, ownedItems.value.get(uuid)![2]);
|
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.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
<template>
|
||||||
<h1>Congratulations</h1>
|
<div class="container" style="display: flex; flex-flow: column; margin-top: 1.25rem;">
|
||||||
<p>This is todo.</p>
|
<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>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
Reference in New Issue
Block a user