109 lines
2.2 KiB
Vue
109 lines
2.2 KiB
Vue
<template>
|
||
<el-card shadow="never" class="todo-card">
|
||
<div class="section-header">
|
||
<span>个人待办(本地)</span>
|
||
</div>
|
||
<div class="input-row">
|
||
<el-input v-model="input" placeholder="记录一个备忘..." @keyup.enter.native="add" />
|
||
<el-button type="primary" @click="add">添加</el-button>
|
||
</div>
|
||
<div v-if="todos.length" class="todo-list">
|
||
<div v-for="item in todos" :key="item.id" class="todo-item">
|
||
<el-checkbox v-model="item.done" @change="save">{{ item.text }}</el-checkbox>
|
||
<el-button link type="danger" size="small" @click="remove(item.id)">删除</el-button>
|
||
</div>
|
||
</div>
|
||
<div v-else class="empty">暂无待办,可添加你的个人备忘</div>
|
||
</el-card>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, ref, watch } from "vue";
|
||
|
||
const props = defineProps<{ storageKey: string | undefined }>();
|
||
|
||
interface TodoItem {
|
||
id: string;
|
||
text: string;
|
||
done: boolean;
|
||
}
|
||
|
||
const input = ref("");
|
||
const todos = ref<TodoItem[]>([]);
|
||
|
||
const load = () => {
|
||
if (!props.storageKey) return;
|
||
const raw = localStorage.getItem(props.storageKey);
|
||
if (raw) {
|
||
try {
|
||
todos.value = JSON.parse(raw) || [];
|
||
} catch {
|
||
todos.value = [];
|
||
}
|
||
}
|
||
};
|
||
|
||
const save = () => {
|
||
if (!props.storageKey) return;
|
||
localStorage.setItem(props.storageKey, JSON.stringify(todos.value));
|
||
};
|
||
|
||
const add = () => {
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
todos.value.unshift({
|
||
id: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
|
||
text,
|
||
done: false,
|
||
});
|
||
input.value = "";
|
||
save();
|
||
};
|
||
|
||
const remove = (id: string) => {
|
||
todos.value = todos.value.filter((t) => t.id !== id);
|
||
save();
|
||
};
|
||
|
||
watch(
|
||
() => props.storageKey,
|
||
() => load(),
|
||
{ immediate: true }
|
||
);
|
||
|
||
onMounted(() => {
|
||
load();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.todo-card {
|
||
min-height: 200px;
|
||
}
|
||
.section-header {
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
.input-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.todo-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.todo-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 6px 8px;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 6px;
|
||
}
|
||
.empty {
|
||
color: #888;
|
||
}
|
||
</style>
|