待办

经验创意 · 164 次浏览
ai56 创建于 2026-05-19 20:08

待办

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>燕幕·日程待办</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            user-select: none; /* 避免拖拽时选中文本, 宿主外框负责拖拽 */
        }

        html, body {
            margin: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
            background: transparent;
        }

        body {
            font-family: "Microsoft YaHei", "Segoe UI", "PingFang SC", Roboto, sans-serif;
            background: transparent;
        }

        /* 主卡片:完全填充宿主区域,深色质感渐变 */
        .card {
            width: 100%;
            height: 100%;
            background: radial-gradient(ellipse at 30% 20%, #1A2A32, #0E1A1F);
            border-radius: 18px;
            border: 1px solid rgba(120, 180, 200, 0.35);
            box-shadow: inset 0 1px 0.5px rgba(255, 255, 255, 0.08), 0 8px 20px rgba(0, 0, 0, 0.3);
            box-sizing: border-box;
            display: flex;
            flex-direction: column;
            backdrop-filter: blur(0px); /* 纯透明层不使用blur,避免文字模糊, 保持锐利 */
            position: relative;
            overflow: hidden;
        }

        /* 顶部操作栏 + 标题 */
        .header {
            padding: 12px 16px 8px 18px;
            display: flex;
            justify-content: space-between;
            align-items: baseline;
            border-bottom: 1px solid rgba(70, 130, 150, 0.3);
            background: transparent;
            flex-shrink: 0;
        }

        .title-section {
            display: flex;
            align-items: baseline;
            gap: 8px;
        }

        .title {
            font-size: 1.05rem;
            font-weight: 550;
            letter-spacing: 0.5px;
            background: linear-gradient(135deg, #E0F0F5, #B5D8E8);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            text-shadow: 0 1px 2px rgba(0,0,0,0.1);
        }

        .stats-badge {
            font-size: 0.7rem;
            background: rgba(40, 70, 80, 0.65);
            padding: 2px 8px;
            border-radius: 20px;
            color: #9acad9;
            font-weight: 400;
            backdrop-filter: blur(2px);
        }

        /* 分类标签栏 */
        .tabs {
            display: flex;
            gap: 8px;
            padding: 8px 16px 0 18px;
            flex-wrap: nowrap;
            overflow-x: auto;
            scrollbar-width: thin;
            flex-shrink: 0;
        }

        .tabs::-webkit-scrollbar {
            height: 2px;
        }

        .tab-btn {
            background: rgba(30, 55, 65, 0.7);
            border: none;
            padding: 5px 14px;
            border-radius: 40px;
            font-size: 0.7rem;
            font-weight: 500;
            color: #B8D8E8;
            cursor: pointer;
            transition: all 0.2s ease;
            backdrop-filter: blur(2px);
            white-space: nowrap;
            font-family: inherit;
        }

        .tab-btn.active {
            background: #2F8FAA;
            color: #F0F9FF;
            box-shadow: 0 2px 8px rgba(47, 143, 170, 0.3);
            border: 0.5px solid rgba(255,255,240,0.2);
        }

        .tab-btn:hover:not(.active) {
            background: rgba(55, 100, 115, 0.9);
            color: #E0F0FA;
        }

        /* 新增待办输入区 */
        .input-area {
            padding: 12px 16px 8px 18px;
            display: flex;
            gap: 10px;
            align-items: center;
            flex-shrink: 0;
        }

        .task-input {
            flex: 1;
            background: rgba(0, 0, 0, 0.35);
            border: 1px solid rgba(80, 140, 160, 0.6);
            border-radius: 28px;
            padding: 8px 14px;
            font-size: 0.8rem;
            color: #EFF9FF;
            font-family: inherit;
            outline: none;
            transition: all 0.2s;
            backdrop-filter: blur(4px);
        }

        .task-input:focus {
            border-color: #4AB3D0;
            background: rgba(0, 0, 0, 0.5);
            box-shadow: 0 0 0 1px rgba(74, 179, 208, 0.3);
        }

        .task-input::placeholder {
            color: #8aabb8;
            font-size: 0.75rem;
        }

        .add-btn {
            background: #2F8FAA;
            border: none;
            width: 34px;
            height: 34px;
            border-radius: 34px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: 0.18s;
            color: white;
            font-size: 1.3rem;
            font-weight: 400;
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
        }

        .add-btn:hover {
            background: #40B0CE;
            transform: scale(0.96);
        }

        .add-btn:active {
            background: #1F6F88;
        }

        /* 时间段快捷提醒区 (贴片风格) */
        .time-block-section {
            padding: 5px 18px 5px 18px;
            flex-shrink: 0;
        }

        .time-slot-row {
            background: rgba(25, 50, 60, 0.6);
            backdrop-filter: blur(6px);
            border-radius: 40px;
            padding: 6px 12px;
            display: flex;
            gap: 12px;
            align-items: center;
            justify-content: space-between;
            border: 0.5px solid rgba(90, 160, 180, 0.4);
        }

        .time-slot-label {
            font-size: 0.7rem;
            font-weight: 500;
            color: #C0E0ED;
            letter-spacing: 0.3px;
            background: rgba(0,0,0,0.3);
            padding: 3px 8px;
            border-radius: 30px;
        }

        .slot-times {
            display: flex;
            gap: 16px;
            font-size: 0.75rem;
            font-weight: 500;
            color: #DBF0F5;
        }

        .slot-item {
            cursor: pointer;
            transition: all 0.1s ease;
            padding: 3px 6px;
            border-radius: 20px;
            background: rgba(30, 70, 85, 0.5);
        }

        .slot-item:hover {
            background: #3C99B5;
            color: white;
            transform: scale(0.98);
        }

        /* 待办列表容器 (内部滚动, 自定义滚动条) */
        .todo-list-container {
            flex: 1;
            overflow-y: auto;
            margin: 4px 12px 12px 12px;
            padding-right: 4px;
            scrollbar-width: thin;
            scrollbar-color: #3C7C8C #1E2F36;
        }

        .todo-list-container::-webkit-scrollbar {
            width: 5px;
        }
        .todo-list-container::-webkit-scrollbar-track {
            background: #1E2F36;
            border-radius: 10px;
        }
        .todo-list-container::-webkit-scrollbar-thumb {
            background: #3C7C8C;
            border-radius: 10px;
        }
        .todo-list-container::-webkit-scrollbar-thumb:hover {
            background: #5fa3b5;
        }

        .todo-item {
            background: rgba(20, 45, 55, 0.55);
            backdrop-filter: blur(4px);
            border-radius: 14px;
            padding: 8px 12px;
            margin-bottom: 8px;
            display: flex;
            align-items: center;
            gap: 12px;
            border: 0.5px solid rgba(100, 170, 190, 0.3);
            transition: all 0.15s;
        }

        .todo-item:hover {
            background: rgba(35, 75, 88, 0.7);
            border-color: rgba(90, 180, 210, 0.5);
        }

        .todo-check {
            width: 18px;
            height: 18px;
            border-radius: 30px;
            border: 1.5px solid #70B8CE;
            background: transparent;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: 0.1s;
            flex-shrink: 0;
        }
        .todo-check.completed {
            background: #3FACCA;
            border-color: #B2E4F5;
            position: relative;
        }
        .todo-check.completed::after {
            content: "✓";
            font-size: 12px;
            color: #05252f;
            font-weight: bold;
        }

        .todo-text {
            flex: 1;
            font-size: 0.8rem;
            color: #E6F3F8;
            word-break: break-word;
        }
        .todo-text.completed-text {
            text-decoration: line-through;
            color: #85A7B3;
        }

        .todo-category-badge {
            background: #2A6F84;
            border-radius: 16px;
            padding: 2px 8px;
            font-size: 0.6rem;
            font-weight: 500;
            color: #DDF4FF;
            flex-shrink: 0;
        }

        .delete-todo {
            opacity: 0.7;
            cursor: pointer;
            font-size: 1rem;
            color: #C7DDE8;
            transition: 0.1s;
            padding: 4px;
            line-height: 1;
            flex-shrink: 0;
        }
        .delete-todo:hover {
            opacity: 1;
            color: #ffb5a0;
            transform: scale(1.05);
        }

        .empty-message {
            text-align: center;
            padding: 30px 12px;
            color: #7FA5B5;
            font-size: 0.75rem;
            letter-spacing: 0.3px;
        }

        /* 进度小计 */
        .footer-stats {
            flex-shrink: 0;
            padding: 8px 18px 12px 18px;
            border-top: 0.5px solid rgba(70, 130, 150, 0.3);
            font-size: 0.65rem;
            color: #97C1D0;
            display: flex;
            justify-content: space-between;
            background: rgba(0,0,0,0.1);
        }
    </style>
</head>
<body>
<div class="card">
    <div class="header">
        <div class="title-section">
            <span class="title">✦ 燕幕待办</span>
            <span class="stats-badge" id="statsPercent">0%</span>
        </div>
        <div style="font-size:0.7rem; opacity:0.7;">今日专注</div>
    </div>

    <!-- 分类筛选 -->
    <div class="tabs" id="tabContainer">
        <button class="tab-btn active" data-cat="all">全部</button>
        <button class="tab-btn" data-cat="工作">工作</button>
        <button class="tab-btn" data-cat="学习">学习</button>
        <button class="tab-btn" data-cat="生活">生活</button>
        <button class="tab-btn" data-cat="其他">其他</button>
        <button class="tab-btn" data-cat="已完成">已完成</button>
    </div>

    <!-- 时间段贴图: 可点击插入固定任务 -->
    <div class="time-block-section">
        <div class="time-slot-row">
            <span class="time-slot-label">⏰ 时段贴片</span>
            <div class="slot-times">
                <span class="slot-item" data-slot="上午 09:00 开会">🌅 09:00 晨会</span>
                <span class="slot-item" data-slot="下午 14:30 深度工作">📌 14:30 专注</span>
                <span class="slot-item" data-slot="晚间 20:00 复习">🌙 20:00 复习</span>
            </div>
        </div>
    </div>

    <!-- 新增输入框 -->
    <div class="input-area">
        <input type="text" class="task-input" id="taskInput" placeholder="写个待办,回车或按 + 添加... (例如: 整理报告 #工作)">
        <button class="add-btn" id="addTaskBtn">+</button>
    </div>

    <!-- 待办列表滚动区 -->
    <div class="todo-list-container" id="todoListContainer">
        <!-- 动态渲染 -->
        <div class="empty-message">✨ 暂无待办,点击时段或添加任务</div>
    </div>

    <div class="footer-stats">
        <span id="completedCount">0</span> / <span id="totalCount">0</span> 已完成
        <span id="todayCompleteTip">今日完成: 0</span>
    </div>
</div>

<script>
    (function() {
        // ---------- 待办数据结构 ----------
        // 每个待办 { id, text, completed, category, createdAt }
        let todos = [];
        let currentFilter = "all";   // all,工作,学习,生活,其他,已完成
        
        // 关键存储key (燕幕宿主隔离)
        const STORAGE_KEY = "daily_tasks_data";
        
        // 辅助方法: 从字符串解析 #标签 来决定默认分类
        function extractCategoryFromText(rawText) {
            const match = rawText.match(/#(工作|学习|生活|其他)/);
            if(match) return match[1];
            return "其他";  // 默认其他
        }
        
        // 清理文本移除标签符号
        function cleanText(raw) {
            return raw.replace(/#(工作|学习|生活|其他)/g, '').trim() || "未命名任务";
        }
        
        // 保存到宿主 (state.write)
        async function saveTodosToHost() {
            if (!window.yanm) return;
            try {
                await window.yanm.invoke('state.write', { key: STORAGE_KEY, value: JSON.stringify(todos) });
            } catch(e) { console.warn("save state error", e); }
        }
        
        // 从宿主读取数据
        async function loadTodosFromHost() {
            if (!window.yanm) return null;
            try {
                const res = await window.yanm.invoke('state.read', { key: STORAGE_KEY, defaultValue: '[]' });
                if (res && typeof res === 'string') {
                    return JSON.parse(res);
                }
                return null;
            } catch(e) {
                console.warn("load state error", e);
                return null;
            }
        }
        
        // 渲染界面 (根据currentFilter)
        function renderTodos() {
            const container = document.getElementById('todoListContainer');
            if (!container) return;
            
            let filtered = [...todos];
            if (currentFilter === "已完成") {
                filtered = filtered.filter(t => t.completed === true);
            } else if (currentFilter !== "all") {
                filtered = filtered.filter(t => t.category === currentFilter && !t.completed);
            } else {
                filtered = filtered.filter(t => !t.completed); // 全部未完成
            }
            
            if (filtered.length === 0) {
                container.innerHTML = `<div class="empty-message">📭 没有待办,点时段或添加新任务~</div>`;
            } else {
                container.innerHTML = filtered.map(todo => `
                    <div class="todo-item" data-id="${todo.id}">
                        <div class="todo-check ${todo.completed ? 'completed' : ''}" data-action="toggle" data-id="${todo.id}"></div>
                        <div class="todo-text ${todo.completed ? 'completed-text' : ''}">${escapeHtml(todo.text)}</div>
                        <div class="todo-category-badge">${todo.category || '其他'}</div>
                        <div class="delete-todo" data-action="delete" data-id="${todo.id}">🗑️</div>
                    </div>
                `).join('');
            }
            
            // 更新统计
            const totalCount = todos.length;
            const completedCount = todos.filter(t => t.completed === true).length;
            const percent = totalCount === 0 ? 0 : Math.round((completedCount / totalCount) * 100);
            document.getElementById('statsPercent').innerText = `${percent}%`;
            document.getElementById('completedCount').innerText = completedCount;
            document.getElementById('totalCount').innerText = totalCount;
            
            // 今日完成数 (基于 createdAt 日期是今天)
            const todayStr = new Date().toDateString();
            const todayCompleted = todos.filter(t => {
                if (!t.completed) return false;
                const todoDate = new Date(t.createdAt).toDateString();
                return todoDate === todayStr;
            }).length;
            document.getElementById('todayCompleteTip').innerHTML = `今日完成: ${todayCompleted}`;
        }
        
        // 简单防XSS
        function escapeHtml(str) {
            return str.replace(/[&<>]/g, function(m) {
                if(m === '&') return '&amp;';
                if(m === '<') return '&lt;';
                if(m === '>') return '&gt;';
                return m;
            }).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function(c) {
                return c;
            });
        }
        
        // 添加待办
        function addTodo(rawText) {
            if (!rawText || rawText.trim() === '') return false;
            let category = extractCategoryFromText(rawText);
            let cleanContent = cleanText(rawText);
            if (cleanContent === "") cleanContent = "快速任务";
            const newTodo = {
                id: Date.now() + '-' + Math.random().toString(36).substr(2, 6),
                text: cleanContent,
                completed: false,
                category: category,
                createdAt: new Date().toISOString()
            };
            todos.unshift(newTodo);
            renderTodos();
            saveTodosToHost();      // 异步保存不阻塞界面
            return true;
        }
        
        // 切换完成状态
        function toggleComplete(id) {
            const todo = todos.find(t => t.id == id);
            if (todo) {
                todo.completed = !todo.completed;
                renderTodos();
                saveTodosToHost();
            }
        }
        
        // 删除待办
        function deleteTodo(id) {
            todos = todos.filter(t => t.id != id);
            renderTodos();
            saveTodosToHost();
        }
        
        // 事件委托
        function handleContainerClick(e) {
            const target = e.target;
            const action = target.getAttribute('data-action');
            if (action === 'toggle') {
                const id = target.getAttribute('data-id');
                if(id) toggleComplete(id);
            } else if (action === 'delete') {
                const id = target.getAttribute('data-id');
                if(id) deleteTodo(id);
            }
        }
        
        // 时段贴片点击添加
        function bindTimeSlots() {
            document.querySelectorAll('.slot-item').forEach(el => {
                el.addEventListener('click', (e) => {
                    const slotDesc = el.getAttribute('data-slot') || el.innerText;
                    // 根据贴片内容智能分类: 含"开会"工作,"复习"学习,"专注"工作类
                    let raw = slotDesc;
                    if (slotDesc.includes('晨会') || slotDesc.includes('开会')) raw = slotDesc + " #工作";
                    else if (slotDesc.includes('复习')) raw = slotDesc + " #学习";
                    else if (slotDesc.includes('专注')) raw = slotDesc + " #工作";
                    else raw = slotDesc + " #其他";
                    addTodo(raw);
                });
            });
        }
        
        // 分类按钮切换
        function bindTabs() {
            const btns = document.querySelectorAll('.tab-btn');
            btns.forEach(btn => {
                btn.addEventListener('click', () => {
                    const cat = btn.getAttribute('data-cat');
                    if(cat) {
                        currentFilter = cat;
                        btns.forEach(b => b.classList.remove('active'));
                        btn.classList.add('active');
                        renderTodos();
                    }
                });
            });
        }
        
        // 初始化与宿主交互 (重试机制)
        let initRetryCount = 0;
        async function initWithHost() {
            // 先渲染空UI占位,再尝试读宿主数据
            renderTodos();
            
            if (window.yanm && typeof window.yanm.invoke === 'function') {
                // 读取持久化数据
                const loaded = await loadTodosFromHost();
                if (loaded && Array.isArray(loaded)) {
                    todos = loaded;
                } else {
                    // 兜底预设两条样例
                    if(todos.length === 0) {
                        todos = [
                            { id: 'sample1', text: '设计燕幕组件', completed: false, category: '工作', createdAt: new Date().toISOString() },
                            { id: 'sample2', text: '阅读技术文章', completed: false, category: '学习', createdAt: new Date().toISOString() }
                        ];
                        saveTodosToHost();
                    }
                }
                renderTodos();
            } else {
                // 未就绪重试 (setTimeout)
                if (initRetryCount < 15) {
                    initRetryCount++;
                    setTimeout(initWithHost, 200);
                } else {
                    // 完全离线圈: 使用localStorage兜底,但为了保证体验,初始化样例
                    try {
                        const fallback = localStorage.getItem(STORAGE_KEY);
                        if(fallback) todos = JSON.parse(fallback);
                        else if(todos.length === 0) todos = [{ id: 'local1', text: '欢迎使用燕幕待办', completed: false, category: '生活', createdAt: new Date().toISOString() }];
                        renderTodos();
                    } catch(e) {}
                }
                return;
            }
        }
        
        // 监听回车添加、按钮添加
        function bindInputEvents() {
            const inputEl = document.getElementById('taskInput');
            const addBtn = document.getElementById('addTaskBtn');
            if(inputEl) {
                inputEl.addEventListener('keypress', (e) => {
                    if(e.key === 'Enter') {
                        e.preventDefault();
                        const val = inputEl.value.trim();
                        if(val) {
                            addTodo(val);
                            inputEl.value = '';
                        }
                    }
                });
            }
            if(addBtn) {
                addBtn.addEventListener('click', () => {
                    const val = document.getElementById('taskInput').value.trim();
                    if(val) {
                        addTodo(val);
                        document.getElementById('taskInput').value = '';
                    }
                });
            }
        }
        
        // 宿主未提供window.close需求但是添加一个可选项: 不需要主动关闭组件, 但不排除
        // 为了符合规范,不添加额外close
        function mount() {
            bindInputEvents();
            bindTabs();
            bindTimeSlots();
            const containerDiv = document.getElementById('todoListContainer');
            if(containerDiv) containerDiv.addEventListener('click', handleContainerClick);
            initWithHost();
        }
        
        mount();
    })();
</script>
</body>
</html>


回复内容
暂无回复
回复主贴