基于DataviewJS的Tasks查询每周的任务

/* tasks查询工具顶部浮动,hover 出现 */
.block-language-tasks>div{
  position: relative;
}
.plugin-tasks-toolbar {
  position: absolute;
  top: 0;
  right: 0px;
  /* transform: translateX(-50%); */
  /* z-index: 999; */
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

.block-language-tasks:hover .plugin-tasks-toolbar,
.plugin-tasks-toolbar:hover {
  opacity: 1;
  pointer-events: auto;
}

好用

datacore版本 无需启动dataview

```datacorejsx
// Helpers
function getFormattedWeekString(date) {
    const year = date.format("GGGG");
    const week = date.format("WW");
    return `${year}-W${week}`;
}

// Ensures moment locale
window.moment.updateLocale("en", {
    week: { dow: 1 },
});

// 使用 Datacore 内置的 dc.Markdown(底层为 Obsidian MarkdownRenderer),无需启用 Dataview
function TasksRenderer({ query, sourcePath }) {
    const content = "```tasks\n" + query + "\n```";
    return (
        <div style={{ width: "100%" }}>
            <dc.Markdown content={content} sourcePath={sourcePath} inline={false} />
        </div>
    );
}

// Main View Component exported to Datacore
return function WeeklyTaskReport() {
    const fontSizeVar = 0.7;
    const sourcePath = dc.useCurrentPath();
    const today = dc.useMemo(() => window.moment(), []);
    
    // States from global or default
    const [selectedDate, setSelectedDate] = dc.useState(() => {
        return window.tasksQueryData?.selectedDate 
            ? window.moment(window.tasksQueryData.selectedDate) 
            : today.clone();
    });
    const [currentWeekOffset, setCurrentWeekOffset] = dc.useState(() => window.tasksQueryData?.currentWeekOffset || 0);
    const [showTree, setShowTree] = dc.useState(() => window.tasksQueryData?.showTree ?? true);
    const [showWeekTasks, setShowWeekTasks] = dc.useState(() => window.tasksQueryData?.showWeekTasks ?? false);
    const [showInbox, setShowInbox] = dc.useState(() => window.tasksQueryData?.showInbox ?? false);
    const [searchQuery, setSearchQuery] = dc.useState(() => window.tasksQueryData?.searchQuery || "");
    const [searchExpanded, setSearchExpanded] = dc.useState(false);
    const [searchInputVal, setSearchInputVal] = dc.useState(searchQuery);

    // Persist global state when vital view states change
    dc.useEffect(() => {
        window.tasksQueryData = {
            showTree,
            showWeekTasks,
            showInbox,
            currentWeekOffset,
            selectedDate: selectedDate.format("YYYY-MM-DD"),
            searchQuery
        };
    }, [showTree, showWeekTasks, showInbox, currentWeekOffset, selectedDate, searchQuery]);

    // 与周一~周日条一致:周一=0 … 周日=6(与 handleWeekInputChange 中 dayOffset 相同)
    const mondayIndex = (m) => (m.day() === 0 ? 6 : m.day() - 1);

    // Handlers:切换周时同步 selectedDate,否则「周报」query 仍用旧周的 weekStr
    const handlePrevWeek = () => {
        setCurrentWeekOffset((prev) => {
            const next = prev - 1;
            const idx = mondayIndex(selectedDate);
            setSelectedDate(today.clone().startOf("week").add(next, "weeks").add(idx, "days"));
            return next;
        });
    };
    const handleNextWeek = () => {
        setCurrentWeekOffset((prev) => {
            const next = prev + 1;
            const idx = mondayIndex(selectedDate);
            setSelectedDate(today.clone().startOf("week").add(next, "weeks").add(idx, "days"));
            return next;
        });
    };
    const handleToday = () => {
        setCurrentWeekOffset(0);
        setSelectedDate(today.clone());
    };

    const syncWeekOffsetToDate = (m) => {
        const w = m.clone().startOf("week").diff(today.clone().startOf("week"), "weeks");
        setCurrentWeekOffset(w);
    };

    const handlePrevDay = () => {
        const newDate = selectedDate.clone().subtract(1, "day");
        setSelectedDate(newDate);
        syncWeekOffsetToDate(newDate);
    };

    const handleNextDay = () => {
        const newDate = selectedDate.clone().add(1, "day");
        setSelectedDate(newDate);
        syncWeekOffsetToDate(newDate);
    };
    
    const handleWeekInputChange = (e) => {
        const val = e.target.value;
        if (!val) return;
        const [year, week] = val.split('-W').map(str => parseInt(str));
        const firstWeek = today.clone().year(year).startOf('year').week(1);
        const targetWeekStart = firstWeek.add(week - 1, 'weeks');
        const newOffset = targetWeekStart.week() - today.week();
        setCurrentWeekOffset(newOffset);
        
        const dayOffset = selectedDate.day() === 0 ? 6 : selectedDate.day() - 1;
        const newSelectedDate = today.clone().startOf("week").add(newOffset, "weeks").add(dayOffset, "days");
        setSelectedDate(newSelectedDate);
    };

    const handleCreateTask = async () => {
        const tasksApi = app.plugins.plugins['obsidian-tasks-plugin'].apiV1;

        const observer = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    const sectionToRemove = document.querySelector('section.tasks-modal-dependencies-section');
                    if (sectionToRemove) {
                        sectionToRemove.remove();
                        console.log('已删除 section.tasks-modal-dependencies-section');
                        observer.disconnect();
                    }
                }
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });

        let taskLine = await tasksApi.createTaskLineModal();
        if (!taskLine) return;

        observer.disconnect();

        const dailySettings = app.plugins.plugins["periodic-notes"].settings["daily"];
        const dailyFormat = dailySettings['format'];
        const path = `${dailySettings.folder}/${window.moment().format(dailyFormat)}.md`;
        
        let file = app.vault.getFileByPath(path);
        if (!file) {
            const templatePath = dailySettings['template'];
            let data = "";
            if (templatePath) {
                const templateFile = app.vault.getFileByPath(templatePath + ".md");
                if (templateFile) {
                    data = await app.vault.read(templateFile);
                }
            }
            file = await app.vault.create(path, data);
        }

        await app.vault.append(file, taskLine + '\n');
    };

    const toggleTree = () => setShowTree(prev => !prev);
    const toggleWeekTasks = () => {
        setShowWeekTasks(prev => !prev);
        setShowInbox(false);
    };
    const toggleInbox = () => {
        setShowInbox(prev => !prev);
        setShowWeekTasks(false);
    };
    const executeSearch = () => setSearchQuery(searchInputVal);
    const clearSearch = () => {
        setSearchInputVal("");
        setSearchQuery("");
    };

    // Query Generation
    const dateStr = selectedDate.format("YYYY-MM-DD");
    const weekStr = selectedDate.format("YYYY-[W]WW");
    
    const searchFilter = searchQuery.trim() ? `
    filter by function \\
        const searchQueryText = '${searchQuery}'.toLowerCase();\\
        const searchKeywords = searchQueryText.split('\\\\s+').filter(k => k.length > 0);\\
        const taskDesc = task.description.toLowerCase();\\
        const taskFilename = task.file.filenameWithoutExtension.toLowerCase();\\
        const taskTags = task.tags.map(t => t.toLowerCase()).join(' ');\\
        const taskContent = taskDesc + ' ' + taskFilename + ' ' + taskTags;\\
        return searchKeywords.every(keyword => taskContent.includes(keyword));
    ` : "";

    const showTreeOption = showTree ? "show tree" : "";
    const querStr = `OR {(happens in or before ${dateStr}) AND (not done) AND (happens on ${weekStr})} OR {(created in or before ${dateStr}) AND (not done) AND (happens on ${weekStr})}`;

    const queryDayOfWeek = `
    {(done on ${dateStr}) OR (happens on ${dateStr}) OR ( CANCELLED on ${dateStr})}\\
    ${querStr}\\
    OR {filter by function \\
        const filename = task.file.filenameWithoutExtension;\\
        const date1 = window.moment(filename).format('YYYY-MM-DD');\\
        return date1 === '${dateStr}';}
    ${searchFilter}
    ${showTreeOption}

    group by filename
    group by status.name reverse
    short mode
    sort by priority
    is not recurring
    `;

    const queryWeek = `
    group by function task.description.includes("http") ? "🌐阅读记录" : "📅任务记录"
    {(done on ${weekStr}) OR (happens on ${weekStr})}
    ${searchFilter}
    ${showTreeOption}
    is not recurring
    group by done reverse
    short mode
    limit 100
    `;
    
    const queryInboxStr = `
    not done
    group by function task.due.category.groupText
    ${searchFilter}
    ${showTreeOption}
    # 不包含的路径
    path does not include "700【模板】Template"
    # 不包含看板文件的任务
    filter by function !task.file.hasProperty('kanban-plugin')

    short mode
    hide tags
    limit groups 10
    `;

    const finalQuery = showWeekTasks ? queryWeek : (showInbox ? queryInboxStr : queryDayOfWeek);

    // Common Button Style
    const getBtnStyle = (isActive) => ({
        border: "none",
        margin: "0",
        fontSize: `${fontSizeVar}rem`,
        cursor: "pointer",
        padding: "5px 10px",
        color: isActive ? "var(--text-on-accent)" : "var(--text-on-accent)",
        backgroundColor: isActive ? "var(--interactive-accent)" : "var(--interactive-accent)",
    });

    const getToggleBtnStyle = (isActive) => ({
        ...getBtnStyle(true),
        color: isActive ? "var(--text-on-accent)" : "var(--text-normal)",
        backgroundColor: isActive ? "var(--interactive-accent)" : "transparent",
    });

    return (
        <div style={{ width: "100%", display: "flex", flexDirection: "column", alignItems: "center" }}>
            <div style={{ textAlign: "center", marginBottom: "10px", width: "100%" }}>
                {/* Control Buttons Container */}
                <div className="task-control-buttons-container" style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "center", gap: "5px" }}>
                    <button style={getToggleBtnStyle(showInbox)} onClick={toggleInbox}>Inbox</button>
                    <button style={getToggleBtnStyle(showWeekTasks)} onClick={toggleWeekTasks}>周报</button>
                    <button style={getBtnStyle(true)} onClick={handlePrevWeek}>←</button>
                    
                    <input 
                        type="week" 
                        value={getFormattedWeekString(today.clone().startOf('week').add(currentWeekOffset, 'weeks'))}
                        onChange={handleWeekInputChange}
                        style={{
                            fontSize: `${fontSizeVar + 0.2}rem`,
                            color: "var(--text-normal)",
                            backgroundColor: "var(--background-primary)",
                            border: "1px solid var(--background-modifier-border)",
                            borderRadius: "4px",
                            padding: "0.2rem",
                            outline: "none"
                        }} 
                    />
                    
                    <button style={getBtnStyle(true)} onClick={handleNextWeek}>→</button>
                    <button style={getToggleBtnStyle(showTree)} onClick={toggleTree}>↳</button>
                    <button type="button" title="上一天" style={{ ...getBtnStyle(true), padding: "5px 6px" }} onClick={handlePrevDay}>‹</button>
                    <button style={getBtnStyle(true)} onClick={handleToday} onDoubleClick={() => app.commands.executeCommandById("daily-notes")}>今日</button>
                    <button type="button" title="下一天" style={{ ...getBtnStyle(true), padding: "5px 6px" }} onClick={handleNextDay}>›</button>
                    <button style={getBtnStyle(true)} onClick={handleCreateTask}>✚</button>
                    
                    <button 
                        style={{...getBtnStyle(true), padding: "5px 8px", borderRadius: "4px"}} 
                        onClick={() => setSearchExpanded(!searchExpanded)}
                    >
                        🔍
                    </button>
                </div>

                {/* Search Row Container */}
                <div style={{
                    display: "flex", 
                    justifyContent: "center", 
                    alignItems: "center", 
                    width: "100%", 
                    height: searchExpanded ? "auto" : "0", 
                    overflow: "hidden",
                    marginTop: searchExpanded ? "5px" : "0"
                }}>
                    <div style={{
                        display: "flex", 
                        justifyContent: "center", 
                        alignItems: "center", 
                        gap: "3px", 
                        width: searchExpanded ? "100%" : "0", 
                        opacity: searchExpanded ? 1 : 0, 
                        transition: "all 0.2s"
                    }}>
                        <input 
                            type="text" 
                            placeholder="搜索..." 
                            value={searchInputVal}
                            onChange={(e) => setSearchInputVal(e.target.value)}
                            onKeyPress={(e) => e.key === "Enter" && executeSearch()}
                            style={{
                                fontSize: `${fontSizeVar}rem`,
                                color: "var(--text-normal)",
                                backgroundColor: "var(--background-primary)",
                                border: "1px solid var(--background-modifier-border)",
                                borderRadius: "4px",
                                padding: "3px 8px",
                                outline: "none",
                                flex: "1"
                            }} 
                        />
                        <button 
                            style={{...getBtnStyle(true), borderRadius: "4px", padding: "5px 6px", display: searchExpanded ? "block" : "none"}}
                            onClick={clearSearch}
                        >清空</button>
                        <button 
                            style={{...getBtnStyle(true), borderRadius: "4px", padding: "5px 6px", display: searchExpanded ? "block" : "none"}}
                            onClick={executeSearch}
                        >搜索</button>
                    </div>
                </div>
            </div>

            {/* Week Selection Buttons Container */}
            <div className="week-select-buttons-container" style={{ display: "flex", justifyContent: "center", width: "100%" }}>
                {["周一", "周二", "周三", "周四", "周五", "周六", "周天"].map((dayName, index) => {
                    const date = today.clone().startOf("week").add(currentWeekOffset, "weeks").add(index, "days");
                    const isSelected = selectedDate.format("YYYY-MM-DD") === date.format("YYYY-MM-DD");
                    
                    return (
                        <button
                            key={index}
                            className="week-day-button"
                            onClick={() => setSelectedDate(date)}
                            style={{
                                border: "none",
                                borderRadius: "0px",
                                cursor: "pointer",
                                fontSize: `${fontSizeVar}rem`,
                                flex: "1 1 auto",
                                color: isSelected ? "var(--text-on-accent)" : "var(--text-normal)",
                                backgroundColor: isSelected ? "var(--interactive-accent)" : "transparent",
                            }}
                        >
                            {`${dayName}(${date.format("DD")})`}
                        </button>
                    );
                })}
            </div>

            {/* Tasks Plugin Codeblock Render Area */}
            <TasksRenderer query={finalQuery} sourcePath={sourcePath} />
        </div>
    );
}
```

1 个赞