吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3520|回复: 64
收起左侧

[其他原创] 每日任务待办小工具

  [复制链接]
kingyifan 发表于 2025-9-8 15:50
在忙碌的生活和工作中,我们常常会忘记一些重要的事情。 希望有一个简洁、安全、随时都能打开的小工具,来记录当天的待办事项,帮我们专注和自律。
最关键个人数据还不想保存他们云端(数据全在自己本地),所以我自己就做了一个桌面版小工具,并且也给开源了,欢迎大家试用~如果有bug 和需求可以提哦~
开源地址:https://github.com/coder-kingyifan/todo-desktop
桌面版下载地址:
https://github.com/coder-kingyifan/todo-desktop/releases/tag/1.0.0
在线访问地址(推荐使用桌面版):https://coder-kingyifan.github.io/todo/

功能:
1、可以记录每日的todo待办
2、可以切换主题
3、可以置顶不打扰用户(默默会放在用户右上角)
4、支持快速粘贴图片到任务中
5、数据全部保存在本地

这是网页版本代码:
[Asm] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh" class="">
<head>
    <meta charset="UTF-8"/>
    <title>憨憨每日TODO</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
        tailwind.config = {darkMode: 'class'}
    </script>
</head>
<body class="min-h-screen flex items-center justify-center p-4 transition-colors
  bg-gradient-to-br from-green-100 via-emerald-100 to-teal-100
  dark:from-gray-900 dark:via-black dark:to-gray-900">

<!-- 待办页面 -->
<div id="todoPage" class="w-full max-w-3xl bg-white/90 dark:bg-black/70 backdrop-blur-md
      rounded-2xl shadow-2xl p-6 flex flex-col min-h-[85vh] border border-gray-200 dark:border-gray-700">


    <!-- 标题 -->
    <h2 id="pageTitle"
        class="text-center text-3xl font-extrabold mb-4
  bg-gradient-to-r from-green-500 via-emerald-500 to-teal-500
  bg-clip-text text-transparent drop-shadow-lg tracking-wide">
    </h2>


    <!-- 日期 + 星期 + 日期选择器 -->
    <div class="flex items-center justify-center gap-4 mb-6 relative">
        <!-- 日期 + 星期 -->


        <!-- 日期选择器 -->
        <input type="date" id="datePicker"
               class="px-4 py-2 rounded-xl border border-gray-300 dark:border-gray-600
           bg-white dark:bg-gray-800 dark:text-gray-200 shadow-sm hover:shadow-md
           transition focus:outline-none focus:ring-2 focus:ring-green-400"/>
        <h1 id="date"
            class="text-2xl font-bold bg-gradient-to-r from-green-500 to-green-600
           bg-clip-text text-transparent drop-shadow-lg whitespace-nowrap"></h1>
        <!-- 设置按钮固定右上角 -->
        <button
                class="absolute top-0 right-0 bg-green-500 text-white rounded-lg p-2 hover:bg-green-600 shadow-md">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
                 stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                      d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                      d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
            </svg>

        </button>
    </div>


    <!-- 添加TODO -->
    <div class="flex flex-col sm:flex-row gap-3 mb-6">
        <input id="newTask" type="text"
               placeholder="请输入今日TODO(可以直接粘贴图片哦~)"
               class="flex-1 px-4 py-3 rounded-2xl border border-gray-300
               dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100
               focus:outline-none focus:ring-2 focus:ring-green-400
               shadow-sm"/>
        <select id="priority"
                class="px-3 py-2 rounded-xl border border-gray-300 dark:border-gray-600
               dark:bg-gray-800 dark:text-gray-100 shadow-sm">
            <option value="low">&#127793; 低</option>
            <option value="medium">&#9889; 中</option>
            <option value="high">&#128293; 高</option>
        </select>
        <button
                class="px-6 py-2 rounded-xl font-semibold text-white
               bg-green-500 hover:bg-green-600 shadow-md transition">
            添加
        </button>
    </div>


    <!-- 筛选 + 一键完成 -->
    <div class="flex justify-between items-center mb-4">
        <!-- 左边 前一天 -->
        <button
                class="text-sm text-green-600 dark:text-green-300 hover:underline">← 前一天
        </button>

        <!-- 中间 筛选 -->
        <div class="flex gap-2">
            <button id="filter-all"
                    class="px-3 py-1 rounded-lg text-sm text-black dark:text-white">全部
            </button>
            <button id="filter-done"
                    class="px-3 py-1 rounded-lg text-sm text-black dark:text-white">已完成
            </button>
            <button id="filter-undone"
                    class="px-3 py-1 rounded-lg text-sm text-black dark:text-white">未完成
            </button>
        </div>

        <!-- 右边 今天按钮(可隐藏) -->
        <span id="todayBtnWrapper">
    <button
            class="text-sm text-green-600 dark:text-green-300 hover:underline">今天 →</button>
  </span>
    </div>


    <!-- 状态 + 一键完成 -->
    <div class="flex justify-end items-center gap-3 mb-4">
        <div id="stats" class="text-sm text-gray-600 dark:text-gray-300"></div>
        <button
                class="px-3 py-1 text-sm rounded-lg bg-green-500 text-white hover:bg-green-600 transition">一键完成全部
        </button>
    </div>


    <!-- 待办列表 -->
    <ul id="taskList" class="space-y-3 flex-1 overflow-y-auto"></ul>

    <!-- 作者信息 -->
    <div class="mt-6 text-center text-gray-500 dark:text-gray-400 text-sm">
        作者:KingYiFan
    </div>


</div>
</div>

<!-- 设置页面 -->
<div id="settingsPage" class="hidden w-full max-w-md bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6">
    <h2 class="text-xl font-bold mb-4 text-gray-800 dark:text-gray-200">&#9881;&#65039; 设置</h2>

    <div class="mb-4">
        <label class="block text-sm mb-1 text-gray-700 dark:text-gray-300">默认优先级</label>
        <select id="defaultPriority"
                class="w-full border border-gray-300 dark:border-gray-600 rounded-lg px-2 py-2 dark:bg-gray-700 dark:text-gray-100">
            <option value="low">&#127793; 低</option>
            <option value="medium">&#9889; 中</option>
            <option value="high">&#128293; 高</option>
        </select>
    </div>

    <div class="mb-4">
        <label class="block text-sm mb-1 text-gray-700 dark:text-gray-300">默认主题</label>
        <select id="defaultTheme"
                class="w-full border border-gray-300 dark:border-gray-600 rounded-lg px-2 py-2 dark:bg-gray-700 dark:text-gray-100">
            <option value="light">&#9728;&#65039; 浅色</option>
            <option value="dark">&#127769; 深色</option>
        </select>
    </div>

    <div class="mb-6 grid grid-cols-2 gap-3">
        <!-- 导出 -->
        <button
                class="flex items-center justify-center gap-2 w-full px-4 py-3 rounded-2xl font-semibold
                   bg-gradient-to-r from-blue-400 to-blue-600 text-white shadow-lg
                   hover:scale-105 hover:shadow-xl transition">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
                 viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                      d="M12 4v12m0 0l-4-4m4 4l4-4M4 16h16"/>
            </svg>
            导出数据
        </button>

        <!-- 导入 -->
        <input type="file" id="importFile" accept=".json"
               class="hidden"/>
        <button
                class="flex items-center justify-center gap-2 w-full px-4 py-3 rounded-2xl font-semibold
                   bg-gradient-to-r from-purple-400 to-purple-600 text-white shadow-lg
                   hover:scale-105 hover:shadow-xl transition">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
                 viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                      d="M4 12h16m0 0l-4-4m4 4l-4 4"/>
            </svg>
            导入数据
        </button>
    </div>

    <div class="mb-6">
        <button
                class="w-full bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600">&#128465;&#65039; 删除全部数据
        </button>
    </div>


    <div class="flex justify-end">
        <button
                class="bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg hover:from-green-600 hover:to-green-700">
            返回待办
        </button>
    </div>
</div>

<!-- 图片预览 -->
<div id="imgPreview" class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-50">
    <img id="previewImg" src="" class="max-h-[90vh] max-w-[90vw] rounded shadow-xl border-4 border-white"/>
</div>

<script>
    const taskListEl = document.getElementById("taskList");
    const newTaskEl = document.getElementById("newTask");
    const priorityEl = document.getElementById("priority");
    const dateEl = document.getElementById("date");
    const weekdayEl = document.getElementById("weekday");
    const datePicker = document.getElementById("datePicker");
    const statsEl = document.getElementById("stats");
    const imgPreview = document.getElementById("imgPreview");
    const previewImg = document.getElementById("previewImg");

    const todoPage = document.getElementById("todoPage");
    const settingsPage = document.getElementById("settingsPage");
    const defaultPriorityEl = document.getElementById("defaultPriority");
    const defaultThemeEl = document.getElementById("defaultTheme");

    let currentDate = new Date();
    let currentFilter = "all";
    const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];

    function genId() {
        return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
    }

    function formatDate(d) {
        return d.toISOString().split("T")[0];
    }

    function formatDateTime(d) {
        if (!d) return "";
        const dt = new Date(d);
        const todayStr = formatDate(new Date());
        const dateStr = formatDate(dt);
        if (dateStr === todayStr) {
            return dt.toLocaleTimeString("zh-CN", {hour: "2-digit", minute: "2-digit"});
        } else {
            return `${dateStr} ${dt.toLocaleTimeString("zh-CN", {hour: "2-digit", minute: "2-digit"})}`;
        }
    }

    // 导出数据
    function exportData() {
        const data = {};
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            const raw = localStorage.getItem(key);
            try {
                data[key] = JSON.parse(raw); // 如果能解析,说明是 JSON
            } catch {
                data[key] = raw; // 如果不能解析,就直接存原始字符串
            }
        }

        const blob = new Blob([JSON.stringify(data, null, 2)], {type: "application/json"});
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "todo-data.json";
        a.click();
        URL.revokeObjectURL(url);
        showToast("&#9989; 数据已导出");
    }


    // 导入数据
    function importData(event) {
        const file = event.target.files[0];
        if (!file) return;

        const reader = new FileReader();
        reader.onload = e => {
            try {
                const data = JSON.parse(e.target.result);
                for (let key in data) {
                    localStorage.setItem(key, JSON.stringify(data[key]));
                }
                showToast("&#9989; 数据已导入,正在刷新");
                setTimeout(() => location.reload(), 1000);
            } catch (err) {
                alert("导入失败:文件格式错误");
            }
        };
        reader.readAsText(file);
    }


    // ---------------- TODO管理 ----------------
    function loadTasks(dateStr) {
        const d = new Date(dateStr);
        const todayStr = formatDate(new Date());
        const yesterdayStr = formatDate(new Date(Date.now() - 86400000)); // 昨天
        const weekday = weekdays[d.getDay()];

        // 动态标题
        if (dateStr === todayStr) {
            document.querySelector("h2").textContent = "憨憨今日 TODO";
            dateEl.textContent = `${weekday}`;
        } else if (dateStr === yesterdayStr) {
            document.querySelector("h2").textContent = "憨憨昨日 TODO";
            dateEl.textContent = `${weekday}`;
        } else {
            document.querySelector("h2").textContent = "憨憨历史 TODO";
            dateEl.textContent = `${weekday}`;
        }

        // 控制 "今天" 按钮显示
        const todayBtnWrapper = document.getElementById("todayBtnWrapper");
        if (dateStr === todayStr) {
            todayBtnWrapper.classList.add("hidden");
        } else {
            todayBtnWrapper.classList.remove("hidden");
        }

        // 载入任务
        datePicker.value = dateStr;
        let tasks = JSON.parse(localStorage.getItem(dateStr) || "[]");

        // 只在展示时合并前一天未完成的任务,不保存到 localStorage
        const viewingDate = new Date(dateStr);
        const now = new Date();

        // 对于今天及之前的日期,继承前一天未完成的任务
        if (viewingDate <= now) {
            // 获取前一天日期
            const prevDay = new Date(viewingDate);
            prevDay.setDate(prevDay.getDate() - 1);
            const prevDayStr = formatDate(prevDay);

            // 获取前一天的任务
            let previousTasks = JSON.parse(localStorage.getItem(prevDayStr) || "[]");
            let undoneFromPrevious = previousTasks.filter(t => !t.done);

            // 检查避免重复(按 id 判断)
            let existingIds = new Set(tasks.map(t => t.id));
            let newTasks = undoneFromPrevious.filter(t => !existingIds.has(t.id));

            // 将前一天未完成任务添加到当前日期(仅用于展示)
            tasks.push(...newTasks);
        }

        renderTasks(tasks);
    }


    function renderTasks(tasks) {
        taskListEl.innerHTML = "";
        let filtered = tasks;
        if (currentFilter === "done") filtered = tasks.filter(t => t.done);
        if (currentFilter === "undone") filtered = tasks.filter(t => !t.done);

        if (filtered.length === 0) {
            statsEl.textContent = "今日未完成 0 / 0";
            return;
        }

        const prioOrder = {high: 1, medium: 2, low: 3};
        filtered.sort((a, b) => {
            if (a.done !== b.done) return a.done ? 1 : -1;
            if (!a.done && !b.done) return prioOrder[a.priority] - prioOrder[b.priority];
            if (a.done && b.done) return new Date(b.completedAt) - new Date(a.completedAt);
            return 0;
        });

        filtered.forEach(task => {
            const li = document.createElement("li");
            li.className = "flex flex-col bg-gray-50 dark:bg-gray-800 rounded-lg px-3 py-2 shadow-sm mb-2";

            const row = document.createElement("div");
            row.className = "flex items-center";

            const checkbox = document.createElement("input");
            checkbox.type = "checkbox";
            checkbox.checked = task.done;
            checkbox.className = "mr-3 accent-green-500";
            checkbox.onchange = () => toggleTask(task.id);
            row.appendChild(checkbox);

            // === 任务内容 ===
            const input = document.createElement("input");
            input.type = "text";
            input.value = task.text || "";
            input.className = "flex-1 bg-transparent border-b border-dashed border-gray-400 focus:outline-none "
                + (task.done ? "line-through text-gray-400 dark:text-gray-500" : "text-gray-800 dark:text-gray-100");

            if (task.done) {
                input.readOnly = true; // 禁止修改
            } else {
                input.onchange = () => updateTaskText(task.id, input.value);
            }
            row.appendChild(input);

            // === 优先级 ===
            const prioSelect = document.createElement("select");
            prioSelect.className = "ml-2 text-xs rounded px-1 py-0.5 dark:bg-gray-700 dark:text-gray-100";
            ["low", "medium", "high"].forEach(p => {
                const opt = document.createElement("option");
                opt.value = p;
                opt.textContent = p === "high" ? "&#128293; 高" : p === "medium" ? "&#9889; 中" : "&#127793; 低";
                if (task.priority === p) opt.selected = true;
                prioSelect.appendChild(opt);
            });

            if (task.done) {
                prioSelect.disabled = true; // 禁止修改
            } else {
                prioSelect.onchange = () => updateTaskPriority(task.id, prioSelect.value);
            }
            row.appendChild(prioSelect);

            // 添加完成按钮
            if (!task.done) {
                const completeBtn = document.createElement("button");
                completeBtn.innerHTML = "&#10003;";
                completeBtn.className = "ml-2 text-xs px-2 py-1 bg-green-500 text-white rounded hover:bg-green-600";
                completeBtn.title = "标记完成";
                completeBtn.onclick = () => markComplete(task.id);
                row.appendChild(completeBtn);
            }

            // 添加删除按钮
            const deleteBtn = document.createElement("button");
            deleteBtn.innerHTML = "&#128465;&#65039;";
            deleteBtn.className = "ml-2 text-xs px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600";
            deleteBtn.title = "删除任务";
            deleteBtn.onclick = () => deleteTask(task.id);
            row.appendChild(deleteBtn);

            li.appendChild(row);

            if (task.image) {
                const img = document.createElement("img");
                img.src = task.image;
                img.className = "mt-2 max-h-40 rounded border cursor-pointer";
                img.onclick = () => showImage(task.image, [task.image], 0);
                li.appendChild(img);
            }

            const meta = document.createElement("div");
            meta.className = "text-xs text-gray-500 dark:text-gray-400 mt-1";
            meta.textContent = `创建: ${formatDateTime(task.createdAt)} ${task.done ? " | 完成: " + formatDateTime(task.completedAt) : ""}`;
            li.appendChild(meta);

            // ----- 备注 -----
            const noteBox = document.createElement("div");
            noteBox.className = "mt-2 w-full";

            const noteInput = document.createElement("textarea");
            noteInput.rows = 3;
            noteInput.placeholder = "备注(支持粘贴多张图片哦~)";
            noteInput.value = task.note?.text || "";
            noteInput.className = "w-full text-sm px-3 py-2 rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 resize-none";

            if (task.done) {
                noteInput.readOnly = false; // 禁止输入
            } else {
                noteInput.onchange = () => updateNoteText(task.id, noteInput.value);
                noteInput.addEventListener("paste", e => {
                    const items = e.clipboardData.items;
                    for (let item of items) {
                        if (item.type.startsWith("image")) {
                            const file = item.getAsFile();
                            const reader = new FileReader();
                            reader.onload = evt => addNoteImage(task.id, evt.target.result);
                            reader.readAsDataURL(file);
                            e.preventDefault();
                        }
                    }
                });
            }
            noteBox.appendChild(noteInput);

            const noteImgBox = document.createElement("div");
            noteImgBox.className = "flex flex-wrap gap-2 mt-2";
            (task.note?.images || []).forEach((imgSrc, idx) => {
                const wrapper = document.createElement("div");
                wrapper.className = "relative";

                const img = document.createElement("img");
                img.src = imgSrc;
                img.className = "h-16 rounded border cursor-pointer";
                img.onclick = () => showImage(imgSrc, task.note.images, idx);
                wrapper.appendChild(img);

                if (!task.done) {
                    const delBtn = document.createElement("button");
                    delBtn.textContent = "&#10005;";
                    delBtn.className = "absolute top-0 right-0 text-xs bg-red-500 text-white rounded-full px-1";
                    delBtn.onclick = () => removeNoteImage(task.id, idx);
                    wrapper.appendChild(delBtn);
                }

                noteImgBox.appendChild(wrapper);
            });
            noteBox.appendChild(noteImgBox);
            li.appendChild(noteBox);

            taskListEl.appendChild(li);
        });

        const undoneCount = tasks.filter(t => !t.done).length;
        statsEl.textContent = `今日未完成 ${undoneCount} / ${tasks.length}`;
    }


    // === 新增函数 ===
    function updateTaskText(id, newText) {
        // 找到任务实际存储的日期
        let taskDate = formatDate(currentDate);
        let tasks = JSON.parse(localStorage.getItem(taskDate) || "[]");
        let taskExists = tasks.some(t => t.id === id);

        // 如果在当前日期找不到,需要查找任务实际存储的日期
        if (!taskExists) {
            // 遍历所有 localStorage 项查找任务
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                // 检查是否为日期格式的键
                if (key && /^\d{4}-\d{2}-\d{2}$/.test(key)) {
                    let dateTasks = JSON.parse(localStorage.getItem(key) || "[]");
                    if (dateTasks.some(t => t.id === id)) {
                        taskDate = key;
                        tasks = dateTasks;
                        break;
                    }
                }
            }
        }

        tasks = tasks.map(t => t.id === id ? {...t, text: newText} : t);
        localStorage.setItem(taskDate, JSON.stringify(tasks));
    }


    function updateTaskPriority(id, priority) {
        // 找到任务实际存储的日期
        let taskDate = formatDate(currentDate);
        let tasks = JSON.parse(localStorage.getItem(taskDate) || "[]");
        let taskExists = tasks.some(t => t.id === id);

        // 如果在当前日期找不到,需要查找任务实际存储的日期
        if (!taskExists) {
            // 遍历所有 localStorage 项查找任务
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                // 检查是否为日期格式的键
                if (key && /^\d{4}-\d{2}-\d{2}$/.test(key)) {
                    let dateTasks = JSON.parse(localStorage.getItem(key) || "[]");
                    if (dateTasks.some(t => t.id === id)) {
                        taskDate = key;
                        tasks = dateTasks;
                        break;
                    }
                }
            }
        }

        tasks = tasks.map(t => t.id === id ? {...t, priority} : t);
        localStorage.setItem(taskDate, JSON.stringify(tasks));
        loadTasks(formatDate(currentDate)); // 重新加载以确保排序正确
    }


    function showToast(msg) {
        const toast = document.getElementById("toast");
        toast.textContent = msg;
        toast.classList.remove("hidden");
        // 动画:淡入
        setTimeout(() => toast.classList.add("opacity-100"), 10);

        // 2 秒后淡出
        setTimeout(() => {
            toast.classList.remove("opacity-100");
            setTimeout(() => toast.classList.add("hidden"), 300);
        }, 2000);
    }

    // 删除任务函数
    function deleteTask(id) {
        if (!confirm("确定要删除这个TODO吗?")) return;

        // 找到任务实际存储的日期
        let taskDate = formatDate(currentDate);
        let tasks = JSON.parse(localStorage.getItem(taskDate) || "[]");
        let taskExists = tasks.some(t => t.id === id);

        // 如果在当前日期找不到,需要查找任务实际存储的日期
        if (!taskExists) {
            // 遍历所有 localStorage 项查找任务
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                // 检查是否为日期格式的键
                if (key && /^\d{4}-\d{2}-\d{2}$/.test(key)) {
                    let dateTasks = JSON.parse(localStorage.getItem(key) || "[]");
                    if (dateTasks.some(t => t.id === id)) {
                        taskDate = key;
                        tasks = dateTasks;
                        break;
                    }
                }
            }
        }

        // 从实际存储日期删除任务
        tasks = tasks.filter(t => t.id !== id);
        localStorage.setItem(taskDate, JSON.stringify(tasks));

        // 重新加载当前视图的任务
        loadTasks(formatDate(currentDate));
        showToast("TODO已删除");
    }


    function saveTasks(tasks) {
        localStorage.setItem(formatDate(currentDate), JSON.stringify(tasks));
    }

    function addTask(imageData = null) {
        const text = newTaskEl.value.trim();
        if (!text && !imageData) return;
        let tasks = JSON.parse(localStorage.getItem(formatDate(currentDate)) || "[]");
        tasks.push({
            id: genId(),
            text,
            image: imageData,
            done: false,
            createdAt: new Date().toISOString(),
            completedAt: null,
            priority: priorityEl.value,
            note: {text: "", images: []}
        });
        saveTasks(tasks);
        newTaskEl.value = "";
        renderTasks(tasks);
        showToast("TODO已添加 ");
    }

    function toggleTask(id) {
        // 找到任务实际存储的日期
        let taskDate = formatDate(currentDate);
        let tasks = JSON.parse(localStorage.getItem(taskDate) || "[]");
        let taskExists = tasks.some(t => t.id === id);

        // 如果在当前日期找不到,需要查找任务实际存储的日期
        if (!taskExists) {
            // 遍历所有 localStorage 项查找任务
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                // 检查是否为日期格式的键
                if (key && /^\d{4}-\d{2}-\d{2}$/.test(key)) {
                    let dateTasks = JSON.parse(localStorage.getItem(key) || "[]");
                    if (dateTasks.some(t => t.id === id)) {
                        taskDate = key;
                        tasks = dateTasks;
                        break;
                    }
                }
            }
        }

        // 更新任务状态
        tasks = tasks.map(t => t.id === id ? {
            ...t,
            done: !t.done,
            completedAt: !t.done ? new Date().toISOString() : null
        } : t);

        localStorage.setItem(taskDate, JSON.stringify(tasks));
        loadTasks(formatDate(currentDate));
    }


    function markComplete(id) {
        // 找到任务实际存储的日期
        let taskDate = formatDate(currentDate);
        let tasks = JSON.parse(localStorage.getItem(taskDate) || "[]");
        let taskExists = tasks.some(t => t.id === id);

        // 如果在当前日期找不到,需要查找任务实际存储的日期
        if (!taskExists) {
            // 遍历所有 localStorage 项查找任务
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                // 检查是否为日期格式的键
                if (key && /^\d{4}-\d{2}-\d{2}$/.test(key)) {
                    let dateTasks = JSON.parse(localStorage.getItem(key) || "[]");
                    if (dateTasks.some(t => t.id === id)) {
                        taskDate = key;
                        tasks = dateTasks;
                        break;
                    }
                }
            }
        }

        // 标记任务为完成
        tasks = tasks.map(t => t.id === id ? {
            ...t,
            done: true,
            completedAt: t.completedAt || new Date().toISOString()
        } : t);

        localStorage.setItem(taskDate, JSON.stringify(tasks));
        loadTasks(formatDate(currentDate));
    }


    // ---- 备注处理 ----
    function updateNoteText(id, text) {
        let tasks = JSON.parse(localStorage.getItem(formatDate(currentDate)) || "[]");
        tasks = tasks.map(t => t.id === id ? {...t, note: {...(t.note || {images: []}), text}} : t);
        saveTasks(tasks);
    }

    function addNoteImage(id, imgData) {
        let tasks = JSON.parse(localStorage.getItem(formatDate(currentDate)) || "[]");
        tasks = tasks.map(t => {
            if (t.id === id) {
                const images = t.note?.images || [];
                return {...t, note: {...(t.note || {text: ""}), images: [...images, imgData]}};
            }
            return t;
        });
        saveTasks(tasks);
        renderTasks(tasks);
    }

    function removeNoteImage(id, idx) {
        let tasks = JSON.parse(localStorage.getItem(formatDate(currentDate)) || "[]");
        tasks = tasks.map(t => {
            if (t.id === id) {
                const images = [...(t.note?.images || [])];
                images.splice(idx, 1);
                return {...t, note: {...(t.note || {text: ""}), images}};
            }
            return t;
        });
        saveTasks(tasks);
        renderTasks(tasks);
    }

    function completeAllToday() {
        let dateStr = formatDate(currentDate);
        let tasks = JSON.parse(localStorage.getItem(dateStr) || "[]");
        const now = new Date().toISOString();

        let undoneTasks = tasks.filter(t => !t.done).length;
        if (undoneTasks === 0) {
            showToast("&#127881; 今天没有未完成的TODO!");
            return;
        }

        tasks = tasks.map(t => t.done ? t : {...t, done: true, completedAt: now});
        saveTasks(tasks);
        renderTasks(tasks);

        showToast(`一键完成,共完成 ${undoneTasks} 个TODO`);
    }


    // ---------------- 页面 & 设置 ----------------
    function prevDay() {
        currentDate.setDate(currentDate.getDate() - 1);
        loadTasks(formatDate(currentDate));
    }

    function today() {
        currentDate = new Date();
        loadTasks(formatDate(currentDate));
    }

    function setFilter(f) {
        currentFilter = f;
        document.querySelectorAll("[id^=filter-]").forEach(btn =>
            btn.classList.remove("bg-green-200", "dark:bg-green-700", "bg-lime-200", "dark:bg-lime-700", "bg-red-200", "dark:bg-red-700"));
        if (f === "all") document.getElementById("filter-all").classList.add("bg-green-200", "dark:bg-green-700", "text-black", "dark:text-white");
        if (f === "done") document.getElementById("filter-done").classList.add("bg-lime-200", "dark:bg-lime-700", "text-black", "dark:text-white");
        if (f === "undone") document.getElementById("filter-undone").classList.add("bg-red-200", "dark:bg-red-700", "text-black", "dark:text-white");
        renderTasks(JSON.parse(localStorage.getItem(formatDate(currentDate)) || "[]"));
    }

    function openSettings() {
        todoPage.classList.add("hidden");
        settingsPage.classList.remove("hidden");
        defaultPriorityEl.value = localStorage.getItem("defaultPriority") || "medium";
        defaultThemeEl.value = localStorage.getItem("theme") || "light";
    }

    defaultThemeEl.addEventListener("change", () => {
        localStorage.setItem("theme", defaultThemeEl.value);
        if (defaultThemeEl.value === "dark") {
            document.documentElement.classList.add("dark");
        } else {
            document.documentElement.classList.remove("dark");
        }
    });

    function closeSettings() {
        localStorage.setItem("defaultPriority", defaultPriorityEl.value);
        localStorage.setItem("theme", defaultThemeEl.value);

        if (defaultThemeEl.value === "dark") document.documentElement.classList.add("dark");
        else document.documentElement.classList.remove("dark");

        // 同步优先级到主页面
        priorityEl.value = defaultPriorityEl.value;

        todoPage.classList.remove("hidden");
        settingsPage.classList.add("hidden");
        loadTasks(formatDate(currentDate));
    }

    function clearAllData() {
        if (confirm("憨憨温馨提示:确定要删除所有TODO数据吗?此操作不可恢复!!!")) {
            localStorage.clear();
            location.reload();
        }
    }

    // ---------------- 图片预览 ----------------
    let previewImages = [];
    let currentPreviewIndex = 0;

    function showImage(src, allImages = [src], index = 0) {
        if (!allImages || allImages.length === 0) return;
        previewImages = allImages;
        currentPreviewIndex = index;
        previewImg.src = src;
        imgPreview.classList.remove("hidden");
    }

    // 点击遮罩关闭
    imgPreview.onclick = e => {
        if (e.target === imgPreview) { // 只点背景时才关闭
            imgPreview.classList.add("hidden");
        }
    };

    // 键盘控制
    document.addEventListener("keydown", e => {
        if (imgPreview.classList.contains("hidden")) return;

        if (e.key === "Escape") {
            imgPreview.classList.add("hidden");
        }
        if (e.key === "ArrowLeft") {
            currentPreviewIndex = (currentPreviewIndex - 1 + previewImages.length) % previewImages.length;
            previewImg.src = previewImages[currentPreviewIndex];
        }
        if (e.key === "ArrowRight") {
            currentPreviewIndex = (currentPreviewIndex + 1) % previewImages.length;
            previewImg.src = previewImages[currentPreviewIndex];
        }
    });


    imgPreview.onclick = () => imgPreview.classList.add("hidden");
    newTaskEl.addEventListener("paste", e => {
        const items = e.clipboardData.items;
        for (let item of items) {
            if (item.type.startsWith("image")) {
                const file = item.getAsFile();
                const reader = new FileReader();
                reader.onload = evt => addTask(evt.target.result);
                reader.readAsDataURL(file);
                e.preventDefault();
            }
        }
    });

    // ---------------- 初始化 ----------------
    newTaskEl.addEventListener("keydown", e => {
        if (e.key === "Enter") addTask();
    });
    datePicker.addEventListener("change", function () {
        currentDate = new Date(this.value);
        loadTasks(formatDate(currentDate));
    });
    if (localStorage.getItem("theme") === "dark") document.documentElement.classList.add("dark");
    priorityEl.value = localStorage.getItem("defaultPriority") || "medium";
    loadTasks(formatDate(currentDate));
    setFilter("all");
</script>
<!-- 中间提示框 -->
<div id="toast"
     class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2
         bg-black/80 text-white px-6 py-3 rounded-2xl shadow-lg hidden z-50
         text-lg font-semibold opacity-0 transition-opacity duration-300">
</div>
</body>
</html>

免费评分

参与人数 13吾爱币 +17 热心值 +10 收起 理由
jingling4784 + 1 + 1 我很赞同!
feiyully11 + 1 + 1 谢谢@Thanks!
liyicha + 1 谢谢@Thanks!
zhanbuzhu + 1 我很赞同!
习惯大神 + 1 + 1 我很赞同!
Alex_107 + 1 + 1 热心回复!
fanny188 + 1 热心回复!
gblier + 1 谢谢@Thanks!
nianba + 1 + 1 我很赞同!
hrh123 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
qck + 1 + 1 谢谢@Thanks!
txd0920 + 1 谢谢@Thanks!
congcongzhidao + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

azusys 发表于 2025-9-9 08:40
建议增加周期性提醒比如每日,工作日,每周,每月,每季度,每年等
 楼主| kingyifan 发表于 2025-9-8 16:01
naixubao 发表于 2025-9-8 15:56
感谢楼主分享,下载地址打不开,有没有其他的下载地址?

通过网盘分享的文件:
链接: https://pan.baidu.com/s/1Cj-GvtmWh92vOMhlnfUJpQ?pwd=g8sw 提取码: g8sw 复制这段内容后打开百度网盘手机App,操作更方便哦
知心 发表于 2025-9-8 20:24
补张图,楼主的UI做的挺好看的,建议下次直接带图
wechat_2025-09-08_202341_623.png

免费评分

参与人数 1吾爱币 +1 收起 理由
fanny188 + 1 我很赞同!

查看全部评分

myrepent 发表于 2025-9-8 15:52
哇哦,谢谢分享!!
学习学习,简直是帮了大忙
pleny 发表于 2025-9-8 15:53
提高效率的小工具,谢谢。
Yanpeen 发表于 2025-9-8 15:56
如果待办太多了,有无搜索功能咧
 楼主| kingyifan 发表于 2025-9-8 15:57
Yanpeen 发表于 2025-9-8 15:56
如果待办太多了,有无搜索功能咧

需求我记下了,预计下个版本增加,今天新增了v1.0.1版本,增加了删除待办逻辑。在一点点优化,欢迎大家提需求,只要合理,我会尽量满足大家。
alairdng 发表于 2025-9-8 16:06
谢谢分享!!
YukiQ 发表于 2025-9-8 16:07
可以记录重复的待办事项吗?
类似于每天创建一个待办提醒我练习这样子
cqu20104225 发表于 2025-9-8 16:07
感谢楼主分享
我是刘德华 发表于 2025-9-8 16:08
怎么不贴个主界面
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-3-18 04:47

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表