aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--tiddlers/plugin-copy-codeblock-widget.tid (renamed from tiddlers/copy-codeblock.tid)0
-rw-r--r--tiddlers/plugin-current-tiddler-toc-sidebar.tid (renamed from tiddlers/sidebar-current-tiddler-toc.tid)0
-rw-r--r--tiddlers/plugin-current-tiddler-toc-widget.tid (renamed from tiddlers/current-tiddler-toc-widget.tid)0
-rw-r--r--tiddlers/plugin-files-api-route.tid (renamed from tiddlers/files-list-api.tid)2
-rw-r--r--tiddlers/plugin-highlight-caddyfile.tid (renamed from tiddlers/highlight-caddyfile.tid)2
-rw-r--r--tiddlers/plugin-highlight-json5.tid (renamed from tiddlers/highlight-json5.tid)2
-rw-r--r--tiddlers/plugin-highlight-tiddlywiki.tid (renamed from tiddlers/highlight-tiddlywiki.tid)2
-rw-r--r--tiddlers/plugin-journal-calendar-widget.tid (renamed from tiddlers/journal-calendar-widget.tid)0
-rw-r--r--tiddlers/plugin-task-board-edit-fields.tid38
-rw-r--r--tiddlers/plugin-task-board-styles.tid641
-rw-r--r--tiddlers/plugin-task-board-view-fields.tid14
-rw-r--r--tiddlers/plugin-task-board-widget.tid729
-rw-r--r--tiddlers/事情看板.tid6
13 files changed, 1432 insertions, 4 deletions
diff --git a/tiddlers/copy-codeblock.tid b/tiddlers/plugin-copy-codeblock-widget.tid
index 2251c62..2251c62 100644
--- a/tiddlers/copy-codeblock.tid
+++ b/tiddlers/plugin-copy-codeblock-widget.tid
diff --git a/tiddlers/sidebar-current-tiddler-toc.tid b/tiddlers/plugin-current-tiddler-toc-sidebar.tid
index 9d73d6d..9d73d6d 100644
--- a/tiddlers/sidebar-current-tiddler-toc.tid
+++ b/tiddlers/plugin-current-tiddler-toc-sidebar.tid
diff --git a/tiddlers/current-tiddler-toc-widget.tid b/tiddlers/plugin-current-tiddler-toc-widget.tid
index a68e449..a68e449 100644
--- a/tiddlers/current-tiddler-toc-widget.tid
+++ b/tiddlers/plugin-current-tiddler-toc-widget.tid
diff --git a/tiddlers/files-list-api.tid b/tiddlers/plugin-files-api-route.tid
index 063fddb..6236926 100644
--- a/tiddlers/files-list-api.tid
+++ b/tiddlers/plugin-files-api-route.tid
@@ -129,4 +129,4 @@ exports.handler = function(request,response,state) {
"utf8"
);
});
-}; \ No newline at end of file
+};
diff --git a/tiddlers/highlight-caddyfile.tid b/tiddlers/plugin-highlight-caddyfile.tid
index d36ea30..8427c3f 100644
--- a/tiddlers/highlight-caddyfile.tid
+++ b/tiddlers/plugin-highlight-caddyfile.tid
@@ -62,4 +62,4 @@ hljs.registerLanguage("caddyfile",function(hljs) {
}
]
};
-}); \ No newline at end of file
+});
diff --git a/tiddlers/highlight-json5.tid b/tiddlers/plugin-highlight-json5.tid
index 099df98..fae9bd4 100644
--- a/tiddlers/highlight-json5.tid
+++ b/tiddlers/plugin-highlight-json5.tid
@@ -56,4 +56,4 @@ hljs.registerLanguage("json5",function(hljs) {
}
]
};
-}); \ No newline at end of file
+});
diff --git a/tiddlers/highlight-tiddlywiki.tid b/tiddlers/plugin-highlight-tiddlywiki.tid
index 1d3d53a..dd1a891 100644
--- a/tiddlers/highlight-tiddlywiki.tid
+++ b/tiddlers/plugin-highlight-tiddlywiki.tid
@@ -188,4 +188,4 @@ hljs.registerLanguage("tiddlywiki",function(hljs) {
}
]
};
-}); \ No newline at end of file
+});
diff --git a/tiddlers/journal-calendar-widget.tid b/tiddlers/plugin-journal-calendar-widget.tid
index cd54724..cd54724 100644
--- a/tiddlers/journal-calendar-widget.tid
+++ b/tiddlers/plugin-journal-calendar-widget.tid
diff --git a/tiddlers/plugin-task-board-edit-fields.tid b/tiddlers/plugin-task-board-edit-fields.tid
new file mode 100644
index 0000000..dd8a351
--- /dev/null
+++ b/tiddlers/plugin-task-board-edit-fields.tid
@@ -0,0 +1,38 @@
+title: $:/plugins/local/task-board/edit-fields
+tags: $:/tags/EditTemplate
+list-before: $:/core/ui/EditTemplate/body
+type: text/vnd.tiddlywiki
+
+<$list filter="[<currentTiddler>tag[事情]]">
+<div class="tc-task-field-panel tc-task-edit-fields">
+ <label class="tc-task-field">
+ <span>优先级</span>
+ <$select tiddler=<<currentTiddler>> field="priority" class="tc-task-field-control" aria-label="优先级">
+ <option value="P1">P1 · 高</option>
+ <option value="P2">P2 · 中</option>
+ <option value="P3">P3 · 低</option>
+ </$select>
+ </label>
+ <label class="tc-task-field">
+ <span>状态</span>
+ <$select tiddler=<<currentTiddler>> field="status" class="tc-task-field-control" aria-label="状态">
+ <option value="待处理">待处理</option>
+ <option value="进行中">进行中</option>
+ <option value="等待">等待</option>
+ <option value="完成">完成</option>
+ </$select>
+ </label>
+ <label class="tc-task-field">
+ <span>开始日期</span>
+ <$edit-text tiddler=<<currentTiddler>> field="start" tag="input" type="date" class="tc-edit-texteditor tc-task-field-control" aria-label="开始日期"/>
+ </label>
+ <label class="tc-task-field">
+ <span>计划结束</span>
+ <$edit-text tiddler=<<currentTiddler>> field="end" tag="input" type="date" class="tc-edit-texteditor tc-task-field-control" aria-label="计划结束日期"/>
+ </label>
+ <label class="tc-task-field">
+ <span>截止日期</span>
+ <$edit-text tiddler=<<currentTiddler>> field="due" tag="input" type="date" class="tc-edit-texteditor tc-task-field-control" aria-label="截止日期"/>
+ </label>
+</div>
+</$list>
diff --git a/tiddlers/plugin-task-board-styles.tid b/tiddlers/plugin-task-board-styles.tid
new file mode 100644
index 0000000..f3c81b1
--- /dev/null
+++ b/tiddlers/plugin-task-board-styles.tid
@@ -0,0 +1,641 @@
+title: $:/plugins/local/task-board/styles
+tags: $:/tags/Stylesheet
+
+\rules only filteredtranscludeinline transcludeinline macrodef macrocallinline
+
+.tc-task-board {
+ width: 100%;
+ --task-p1: #d84b4b;
+ --task-p2: <<colour primary>>;
+ --task-p3: #5b9a68;
+}
+
+.tc-task-board-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1.25rem;
+}
+
+.tc-task-board-new {
+ padding: 0.55rem 1rem;
+ color: <<colour background>>;
+ font-weight: 700;
+ background: <<colour primary>>;
+ border-radius: 0.45rem;
+}
+
+.tc-task-board-switcher {
+ display: grid;
+ grid-template-columns: repeat(4,auto);
+ border: 1px solid <<colour table-border>>;
+ border-radius: 999px;
+ overflow: hidden;
+}
+
+.tc-task-board-view-button {
+ padding: 0.45rem 0.8rem;
+ border-right: 1px solid <<colour table-border>>;
+}
+
+.tc-task-board-view-button:last-child {
+ border-right: 0;
+}
+
+.tc-task-board-view-button.tc-selected {
+ color: <<colour background>>;
+ background: <<colour primary>>;
+}
+
+.tc-task-kanban {
+ display: grid;
+ grid-template-columns: repeat(4,minmax(15rem,1fr));
+ gap: 0.75rem;
+ padding-bottom: 0.5rem;
+ overflow-x: auto;
+ scroll-snap-type: x proximity;
+}
+
+.tc-task-kanban-column {
+ min-height: 18rem;
+ padding: 0.7rem;
+ background: <<colour sidebar-tab-background>>;
+ border: 1px solid <<colour table-border>>;
+ border-radius: 0.65rem;
+ scroll-snap-align: start;
+ transition: box-shadow 120ms ease;
+}
+
+.tc-task-kanban-column.tc-drag-over {
+ box-shadow: inset 0 0 0 3px <<colour primary>>;
+}
+
+.tc-task-kanban-heading {
+ margin: 0 0 0.7rem;
+ font-size: 1rem;
+}
+
+.tc-task-kanban-cards {
+ display: grid;
+ gap: 0.55rem;
+}
+
+.tc-task-card {
+ padding: 0.7rem;
+ background: <<colour tiddler-background>>;
+ border: 1px solid <<colour table-border>>;
+ border-radius: 0.5rem;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.08);
+ cursor: grab;
+}
+
+.tc-task-card.tc-dragging {
+ opacity: 0.45;
+}
+
+.tc-task-card-top {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.tc-task-card-title,
+.tc-task-list-link,
+.tc-task-gantt-task-link,
+.tc-task-unscheduled-link {
+ color: <<colour tiddler-link-foreground>>;
+ font-weight: 650;
+ text-align: left;
+}
+
+.tc-task-card-title:hover,
+.tc-task-list-link:hover,
+.tc-task-gantt-task-link:hover,
+.tc-task-unscheduled-link:hover {
+ text-decoration: underline;
+}
+
+.tc-task-priority {
+ display: inline-flex;
+ flex: 0 0 auto;
+ padding: 0.12rem 0.38rem;
+ color: #fff;
+ font-size: 0.72rem;
+ font-weight: 700;
+ border-radius: 999px;
+}
+
+.tc-task-priority-p1 {background: var(--task-p1);}
+.tc-task-priority-p2 {background: var(--task-p2);}
+.tc-task-priority-p3 {background: var(--task-p3);}
+
+.tc-task-card-dates,
+.tc-task-card-tags {
+ margin-top: 0.45rem;
+ color: <<colour muted-foreground>>;
+ font-size: 0.78rem;
+ line-height: 1.45;
+}
+
+.tc-task-issues {
+ display: block;
+ margin-top: 0.4rem;
+ color: #b33a3a;
+ font-size: 0.75rem;
+}
+
+.tc-task-empty-column,
+.tc-task-empty {
+ padding: 1.25rem;
+ color: <<colour muted-foreground>>;
+ text-align: center;
+}
+
+.tc-task-list-wrapper {
+ width: 100%;
+ overflow-x: auto;
+}
+
+.tc-task-list {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.tc-task-list th,
+.tc-task-list td {
+ padding: 0.65rem;
+ text-align: left;
+ vertical-align: top;
+ border-bottom: 1px solid <<colour table-border>>;
+}
+
+.tc-task-list th {
+ white-space: nowrap;
+ background: <<colour table-header-background>>;
+}
+
+.tc-task-list-title {
+ min-width: 12rem;
+}
+
+.tc-task-list-row.tc-task-invalid {
+ background: rgba(216,75,75,0.08);
+}
+
+.tc-task-calendar-controls {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ margin-bottom: 0.8rem;
+}
+
+.tc-task-calendar-title {
+ min-width: 9rem;
+ margin: 0;
+ text-align: center;
+}
+
+.tc-task-month-button {
+ min-height: 2.25rem;
+ padding: 0.35rem 0.65rem;
+ border: 1px solid <<colour table-border>>;
+ border-radius: 999px;
+}
+
+.tc-task-calendar-grid {
+ border: 1px solid <<colour table-border>>;
+ border-radius: 0.5rem;
+ overflow: hidden;
+}
+
+.tc-task-calendar-header,
+.tc-task-calendar-week {
+ display: grid;
+ grid-template-columns: repeat(7,minmax(0,1fr));
+}
+
+.tc-task-calendar-week {
+ position: relative;
+ gap: 1px;
+ background: <<colour table-border>>;
+ border-top: 1px solid <<colour table-border>>;
+}
+
+.tc-task-calendar-weekday {
+ padding: 0.55rem 0.2rem;
+ font-weight: 700;
+ text-align: center;
+ background: <<colour table-header-background>>;
+}
+
+.tc-task-calendar-day {
+ z-index: 0;
+ min-width: 0;
+ min-height: 8rem;
+ padding: 0.35rem;
+ background: <<colour tiddler-background>>;
+}
+
+.tc-task-calendar-blank {
+ background: <<colour sidebar-tab-background>>;
+}
+
+.tc-task-calendar-day-number {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.65rem;
+ height: 1.65rem;
+ margin-bottom: 0.3rem;
+ font-weight: 700;
+ border-radius: 50%;
+}
+
+.tc-task-calendar-today .tc-task-calendar-day-number {
+ color: <<colour background>>;
+ background: <<colour primary>>;
+}
+
+.tc-task-calendar-range,
+.tc-task-calendar-point,
+.tc-task-calendar-milestone {
+ z-index: 1;
+ align-self: center;
+ display: block;
+ width: 100%;
+ min-width: 0;
+ padding: 0.2rem 0.3rem;
+ color: #fff;
+ font-size: 0.72rem;
+ line-height: 1.2;
+ text-align: left;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.tc-task-calendar-range {
+ margin-left: -1px;
+ margin-right: -1px;
+ background: var(--task-p2);
+}
+
+.tc-task-calendar-range[data-priority="P1"] {background: var(--task-p1);}
+.tc-task-calendar-range[data-priority="P3"] {background: var(--task-p3);}
+.tc-task-calendar-point[data-priority="P1"] {background: var(--task-p1);}
+.tc-task-calendar-point[data-priority="P2"] {background: var(--task-p2);}
+.tc-task-calendar-point[data-priority="P3"] {background: var(--task-p3);}
+.tc-task-calendar-range.tc-range-start {border-radius: 0.35rem 0 0 0.35rem;}
+.tc-task-calendar-range.tc-range-end {border-radius: 0 0.35rem 0.35rem 0;}
+.tc-task-calendar-range.tc-range-start.tc-range-end {border-radius: 0.35rem;}
+
+.tc-task-calendar-point {
+ border-radius: 0.35rem;
+}
+
+.tc-task-calendar-milestone {
+ color: #fff;
+ background: #7656b5;
+ border-radius: 0.3rem;
+}
+
+.tc-task-unscheduled {
+ margin-top: 1rem;
+ padding: 0.8rem;
+ background: <<colour sidebar-tab-background>>;
+ border-radius: 0.5rem;
+}
+
+.tc-task-unscheduled h3 {
+ margin: 0 0 0.5rem;
+ font-size: 1rem;
+}
+
+.tc-task-unscheduled-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.45rem;
+}
+
+.tc-task-unscheduled-link {
+ padding: 0.3rem 0.55rem;
+ background: <<colour tiddler-background>>;
+ border-radius: 0.35rem;
+}
+
+.tc-task-gantt-scroll {
+ width: 100%;
+ overflow: auto;
+ border: 1px solid <<colour table-border>>;
+ border-radius: 0.5rem;
+}
+
+.tc-task-gantt {
+ width: max-content;
+ min-width: 100%;
+ --task-gantt-label-width: 18rem;
+}
+
+.tc-task-gantt-row {
+ display: grid;
+ grid-template-columns: var(--task-gantt-label-width) var(--task-gantt-width);
+ min-height: 3.25rem;
+ border-bottom: 1px solid <<colour table-border>>;
+}
+
+.tc-task-gantt-row:last-child {
+ border-bottom: 0;
+}
+
+.tc-task-gantt-label {
+ position: sticky;
+ left: 0;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.45rem;
+ padding: 0.45rem 0.6rem;
+ background: <<colour tiddler-background>>;
+ border-right: 1px solid <<colour table-border>>;
+}
+
+.tc-task-gantt-header {
+ position: sticky;
+ top: 0;
+ z-index: 4;
+ min-height: 4.5rem;
+}
+
+.tc-task-gantt-header .tc-task-gantt-label {
+ z-index: 5;
+ font-weight: 700;
+ background: <<colour table-header-background>>;
+}
+
+.tc-task-gantt-timeline {
+ position: relative;
+ width: var(--task-gantt-width);
+ background-image: repeating-linear-gradient(to right,transparent 0,transparent 29px,<<colour table-border>> 29px,<<colour table-border>> 30px);
+}
+
+.tc-task-gantt-header-timeline {
+ display: grid;
+ grid-template-rows: 2rem 2.5rem;
+ background: <<colour table-header-background>>;
+}
+
+.tc-task-gantt-months {
+ display: flex;
+ width: var(--task-gantt-date-width);
+ height: 2rem;
+}
+
+.tc-task-gantt-month-segment {
+ position: relative;
+ flex: 0 0 auto;
+ height: 2rem;
+ border-left: 2px solid <<colour primary>>;
+}
+
+.tc-task-gantt-month-label {
+ position: sticky;
+ left: calc(var(--task-gantt-label-width) + 0.5rem);
+ z-index: 1;
+ display: inline-flex;
+ align-items: center;
+ height: 2rem;
+ padding: 0 0.45rem;
+ font-size: 0.78rem;
+ font-weight: 700;
+ white-space: nowrap;
+ background: <<colour table-header-background>>;
+}
+
+.tc-task-gantt-days {
+ display: flex;
+ width: var(--task-gantt-date-width);
+ height: 2.5rem;
+ border-top: 1px solid <<colour table-border>>;
+}
+
+.tc-task-gantt-day {
+ flex: 0 0 auto;
+ padding-top: 0.65rem;
+ font-size: 0.72rem;
+ text-align: center;
+ white-space: nowrap;
+ border-right: 1px solid <<colour table-border>>;
+}
+
+.tc-task-gantt-bar {
+ position: absolute;
+ top: 0.8rem;
+ height: 1.65rem;
+ padding: 0;
+ background: var(--task-p2);
+ border-radius: 0.35rem;
+}
+
+.tc-task-gantt-bar[data-priority="P1"] {background: var(--task-p1);}
+.tc-task-gantt-bar[data-priority="P3"] {background: var(--task-p3);}
+
+.tc-task-gantt-bar-label {
+ position: absolute;
+ top: 0.8rem;
+ z-index: 2;
+ display: inline-flex;
+ align-items: center;
+ height: 1.65rem;
+ padding: 0 0.25rem;
+ color: <<colour tiddler-link-foreground>>;
+ font-size: 0.78rem;
+ font-weight: 650;
+ white-space: nowrap;
+}
+
+.tc-task-gantt-bar-label:hover,
+.tc-task-gantt-bar-label:focus {
+ text-decoration: underline;
+}
+
+.tc-task-gantt-due,
+.tc-task-gantt-end {
+ position: absolute;
+ top: 0.95rem;
+ z-index: 2;
+ padding: 0;
+ transform: translateX(-50%);
+ font-size: 1rem;
+}
+
+.tc-task-gantt-due {color: #b33a3a;}
+.tc-task-gantt-end {color: var(--task-p2);}
+
+.tc-task-gantt-today-line {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ z-index: 1;
+ width: 2px;
+ background: #b33a3a;
+ pointer-events: none;
+}
+
+.tc-task-field-panel {
+ display: grid;
+ grid-template-columns: repeat(5,minmax(0,1fr));
+ gap: 0.6rem;
+ margin: 0.7rem 0;
+ padding: 0.75rem;
+ background: <<colour sidebar-tab-background>>;
+ border: 1px solid <<colour table-border>>;
+ border-radius: 0.5rem;
+}
+
+.tc-task-field {
+ display: grid;
+ gap: 0.3rem;
+}
+
+.tc-task-field > span,
+.tc-task-view-field span {
+ color: <<colour muted-foreground>>;
+ font-size: 0.76rem;
+ font-weight: 650;
+}
+
+.tc-task-field-control {
+ width: 100%;
+ min-width: 0;
+ min-height: 2.25rem;
+}
+
+.tc-task-view-field {
+ display: grid;
+ gap: 0.2rem;
+}
+
+@media (max-width: 700px) {
+ .tc-task-board-toolbar {
+ display: grid;
+ gap: 0.65rem;
+ }
+
+ .tc-task-board-new {
+ min-height: 2.75rem;
+ }
+
+ .tc-task-board-switcher {
+ grid-template-columns: repeat(4,minmax(0,1fr));
+ border-radius: 0.5rem;
+ }
+
+ .tc-task-board-view-button {
+ min-width: 0;
+ padding: 0.5rem 0.15rem;
+ }
+
+ .tc-task-kanban {
+ grid-template-columns: repeat(4,minmax(82vw,82vw));
+ }
+
+ .tc-task-list,
+ .tc-task-list tbody,
+ .tc-task-list tr,
+ .tc-task-list td {
+ display: block;
+ }
+
+ .tc-task-list thead {
+ display: none;
+ }
+
+ .tc-task-list-row {
+ margin-bottom: 0.7rem;
+ padding: 0.65rem;
+ border: 1px solid <<colour table-border>>;
+ border-radius: 0.5rem;
+ }
+
+ .tc-task-list td {
+ display: grid;
+ grid-template-columns: 4.5rem minmax(0,1fr);
+ padding: 0.3rem;
+ border: 0;
+ }
+
+ .tc-task-list td::before {
+ content: attr(data-label);
+ color: <<colour muted-foreground>>;
+ font-size: 0.78rem;
+ font-weight: 650;
+ }
+
+ .tc-task-list-title {
+ min-width: 0;
+ }
+
+ .tc-task-calendar-controls {
+ display: grid;
+ grid-template-columns: repeat(3,minmax(0,1fr));
+ }
+
+ .tc-task-calendar-title {
+ grid-column: 1 / -1;
+ grid-row: 1;
+ justify-self: center;
+ }
+
+ .tc-task-month-button {
+ grid-row: 2;
+ min-height: 2.75rem;
+ padding: 0.3rem 0.15rem;
+ }
+
+ .tc-task-calendar-weekday {
+ padding: 0.4rem 0;
+ font-size: 0.75rem;
+ }
+
+ .tc-task-calendar-day {
+ min-height: 6.5rem;
+ padding: 0.15rem;
+ }
+
+ .tc-task-calendar-day-number {
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 0.78rem;
+ }
+
+ .tc-task-calendar-range,
+ .tc-task-calendar-point,
+ .tc-task-calendar-milestone {
+ padding: 0.16rem 0.1rem;
+ font-size: 0.62rem;
+ text-align: center;
+ }
+
+ .tc-task-gantt-row {
+ --task-gantt-label-width: 9rem;
+ }
+
+ .tc-task-gantt-label {
+ padding: 0.4rem;
+ }
+
+ .tc-task-gantt-task-link {
+ max-width: 6rem;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .tc-task-field-panel {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/tiddlers/plugin-task-board-view-fields.tid b/tiddlers/plugin-task-board-view-fields.tid
new file mode 100644
index 0000000..5a2a75a
--- /dev/null
+++ b/tiddlers/plugin-task-board-view-fields.tid
@@ -0,0 +1,14 @@
+title: $:/plugins/local/task-board/view-fields
+tags: $:/tags/ViewTemplate
+list-before: $:/core/ui/ViewTemplate/body
+type: text/vnd.tiddlywiki
+
+<$list filter="[<currentTiddler>tag[事情]]">
+<div class="tc-task-field-panel tc-task-view-fields">
+ <div class="tc-task-view-field"><span>优先级</span><strong><$view field="priority"/></strong></div>
+ <div class="tc-task-view-field"><span>状态</span><strong><$view field="status"/></strong></div>
+ <div class="tc-task-view-field"><span>开始</span><strong><$view field="start"/></strong></div>
+ <div class="tc-task-view-field"><span>计划结束</span><strong><$view field="end"/></strong></div>
+ <div class="tc-task-view-field"><span>截止</span><strong><$view field="due"/></strong></div>
+</div>
+</$list>
diff --git a/tiddlers/plugin-task-board-widget.tid b/tiddlers/plugin-task-board-widget.tid
new file mode 100644
index 0000000..397811b
--- /dev/null
+++ b/tiddlers/plugin-task-board-widget.tid
@@ -0,0 +1,729 @@
+title: $:/plugins/local/task-board/widget.js
+type: application/javascript
+module-type: widget
+
+/*\
+title: $:/plugins/local/task-board/widget.js
+type: application/javascript
+module-type: widget
+
+Task board with kanban, list, calendar, and Gantt views.
+\*/
+(function() {
+"use strict";
+
+var Widget = require("$:/core/modules/widgets/widget.js").widget,
+ VIEW_STATE_TITLE = "$:/state/task-board/view",
+ MONTH_STATE_TITLE = "$:/state/task-board/month",
+ DAY_MS = 24 * 60 * 60 * 1000,
+ DAY_WIDTH = 30,
+ STATUSES = ["待处理","进行中","等待","完成"],
+ PRIORITIES = ["P1","P2","P3"],
+ VIEW_LABELS = {
+ board: "看板",
+ list: "列表",
+ calendar: "日历",
+ gantt: "甘特图"
+ };
+
+var TaskBoardWidget = function(parseTreeNode,options) {
+ this.initialise(parseTreeNode,options);
+};
+
+TaskBoardWidget.prototype = new Widget();
+TaskBoardWidget.prototype.constructor = TaskBoardWidget;
+
+function pad(value) {
+ return value < 10 ? "0" + value : "" + value;
+}
+
+function todayKey() {
+ var now = new Date();
+ return now.getFullYear() + "-" + pad(now.getMonth() + 1) + "-" + pad(now.getDate());
+}
+
+function monthKey(date) {
+ return date.getUTCFullYear() + "-" + pad(date.getUTCMonth() + 1);
+}
+
+function parseDate(value) {
+ var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || ""),
+ date;
+ if(!match) {
+ return null;
+ }
+ date = new Date(0);
+ date.setUTCHours(0,0,0,0);
+ date.setUTCFullYear(Number(match[1]),Number(match[2]) - 1,Number(match[3]));
+ if(date.getUTCFullYear() !== Number(match[1]) || date.getUTCMonth() !== Number(match[2]) - 1 || date.getUTCDate() !== Number(match[3])) {
+ return null;
+ }
+ return date;
+}
+
+function dateKey(date) {
+ return date.getUTCFullYear() + "-" + pad(date.getUTCMonth() + 1) + "-" + pad(date.getUTCDate());
+}
+
+function dayNumber(date) {
+ return Math.floor(date.getTime() / DAY_MS);
+}
+
+function addDays(date,amount) {
+ return new Date(date.getTime() + amount * DAY_MS);
+}
+
+function isValidMonth(value) {
+ var match = /^(\d{4})-(\d{2})$/.exec(value || "");
+ return !!match && Number(match[2]) >= 1 && Number(match[2]) <= 12;
+}
+
+function priorityRank(priority) {
+ var rank = PRIORITIES.indexOf(priority);
+ return rank === -1 ? PRIORITIES.length : rank;
+}
+
+function compareTasks(left,right) {
+ var priorityDifference = priorityRank(left.effectivePriority) - priorityRank(right.effectivePriority);
+ if(priorityDifference) {
+ return priorityDifference;
+ }
+ var leftDue = left.dueDate ? dayNumber(left.dueDate) : Infinity,
+ rightDue = right.dueDate ? dayNumber(right.dueDate) : Infinity;
+ if(leftDue !== rightDue) {
+ return leftDue - rightDue;
+ }
+ return left.title.localeCompare(right.title);
+}
+
+TaskBoardWidget.prototype.render = function(parent,nextSibling) {
+ this.parentDomNode = parent;
+ this.computeAttributes();
+ this.execute();
+
+ this.containerNode = this.document.createElement("div");
+ this.containerNode.className = "tc-task-board";
+ parent.insertBefore(this.containerNode,nextSibling);
+ this.domNodes.push(this.containerNode);
+ this.renderBoard();
+};
+
+TaskBoardWidget.prototype.execute = function() {
+ this.tag = this.getAttribute("tag","事情");
+};
+
+TaskBoardWidget.prototype.createElement = function(tagName,className,text) {
+ var node = this.document.createElement(tagName);
+ if(className) {
+ node.className = className;
+ }
+ if(text !== undefined && text !== null) {
+ node.textContent = text;
+ }
+ return node;
+};
+
+TaskBoardWidget.prototype.createButton = function(label,className,handler) {
+ var button = this.createElement("button",className,label);
+ button.setAttribute("type","button");
+ button.addEventListener("click",handler);
+ return button;
+};
+
+TaskBoardWidget.prototype.setState = function(title,value) {
+ this.wiki.addTiddler({title: title,text: value});
+};
+
+TaskBoardWidget.prototype.getView = function() {
+ var view = this.wiki.getTiddlerText(VIEW_STATE_TITLE,"board");
+ return VIEW_LABELS[view] ? view : "board";
+};
+
+TaskBoardWidget.prototype.getMonth = function() {
+ var value = this.wiki.getTiddlerText(MONTH_STATE_TITLE,"");
+ if(isValidMonth(value)) {
+ return value;
+ }
+ var today = parseDate(todayKey());
+ return monthKey(today);
+};
+
+TaskBoardWidget.prototype.getTasks = function() {
+ var self = this,
+ tasks = [];
+
+ this.wiki.getTiddlersWithTag(this.tag).forEach(function(title) {
+ var tiddler = self.wiki.getTiddler(title);
+ if(!tiddler || tiddler.fields["draft.of"]) {
+ return;
+ }
+ var fields = tiddler.fields,
+ start = fields.start || "",
+ end = fields.end || "",
+ due = fields.due || "",
+ startDate = parseDate(start),
+ endDate = parseDate(end),
+ dueDate = parseDate(due),
+ issues = [],
+ rangeError,
+ dateError,
+ calendarDates = [];
+
+ if(start && !startDate) {
+ issues.push("开始日期无效");
+ }
+ if(end && !endDate) {
+ issues.push("结束日期无效");
+ }
+ if(due && !dueDate) {
+ issues.push("截止日期无效");
+ }
+ if(startDate && endDate && endDate < startDate) {
+ issues.push("结束日期早于开始日期");
+ }
+ dateError = !!((start && !startDate) || (end && !endDate) || (due && !dueDate));
+ rangeError = !!((start && !startDate) || (end && !endDate) || (startDate && endDate && endDate < startDate));
+ [
+ {type: "start",label: "开始",date: startDate},
+ {type: "end",label: "结束",date: endDate},
+ {type: "due",label: "截止",date: dueDate}
+ ].forEach(function(item) {
+ if(item.date) {
+ calendarDates.push(item);
+ }
+ });
+ calendarDates.sort(function(left,right) {
+ return dayNumber(left.date) - dayNumber(right.date);
+ });
+ if(STATUSES.indexOf(fields.status) === -1) {
+ issues.push("状态无效");
+ }
+ if(PRIORITIES.indexOf(fields.priority) === -1) {
+ issues.push("优先级无效");
+ }
+
+ tasks.push({
+ title: title,
+ status: fields.status || "",
+ effectiveStatus: STATUSES.indexOf(fields.status) === -1 ? STATUSES[0] : fields.status,
+ priority: fields.priority || "",
+ effectivePriority: PRIORITIES.indexOf(fields.priority) === -1 ? "P2" : fields.priority,
+ start: start,
+ end: end,
+ due: due,
+ startDate: startDate,
+ endDate: endDate,
+ dueDate: dueDate,
+ rangeValid: !!startDate && !rangeError,
+ rangeError: rangeError,
+ dateError: dateError,
+ calendarDates: calendarDates,
+ tags: (fields.tags || []).filter(function(tag) {return tag !== self.tag;}),
+ issues: issues
+ });
+ });
+
+ return tasks.sort(compareTasks);
+};
+
+TaskBoardWidget.prototype.navigateTo = function(title,node,event) {
+ var bounds = node.getBoundingClientRect();
+ this.dispatchEvent({
+ type: "tm-navigate",
+ navigateTo: title,
+ navigateFromTitle: this.getVariable("storyTiddler"),
+ navigateFromNode: this,
+ navigateFromClientRect: {
+ top: bounds.top,
+ left: bounds.left,
+ width: bounds.width,
+ right: bounds.right,
+ bottom: bounds.bottom,
+ height: bounds.height
+ },
+ navigateSuppressNavigation: event && (event.metaKey || event.ctrlKey || event.button === 1)
+ });
+};
+
+TaskBoardWidget.prototype.createTaskLink = function(task,className,label) {
+ var self = this,
+ button = this.createButton(label !== undefined ? label : task.title,"tc-btn-invisible " + className,function(event) {
+ self.navigateTo(task.title,event.currentTarget,event);
+ });
+ button.title = "打开事情:" + task.title;
+ button.setAttribute("aria-label","打开事情:" + task.title);
+ return button;
+};
+
+TaskBoardWidget.prototype.renderBoard = function() {
+ var tasks = this.getTasks(),
+ view = this.getView();
+ this.renderToolbar(view);
+ if(view === "list") {
+ this.renderList(tasks);
+ } else if(view === "calendar") {
+ this.renderCalendar(tasks);
+ } else if(view === "gantt") {
+ this.renderGantt(tasks);
+ } else {
+ this.renderKanban(tasks);
+ }
+};
+
+TaskBoardWidget.prototype.renderToolbar = function(currentView) {
+ var self = this,
+ toolbar = this.createElement("div","tc-task-board-toolbar"),
+ switcher = this.createElement("div","tc-task-board-switcher");
+
+ toolbar.appendChild(this.createButton("+ 新建事情","tc-btn-invisible tc-task-board-new",function() {
+ self.dispatchEvent({
+ type: "tm-new-tiddler",
+ paramObject: {
+ tags: $tw.utils.stringifyList([self.tag]),
+ priority: "P2",
+ status: "待处理",
+ start: todayKey()
+ },
+ navigateFromTitle: self.getVariable("storyTiddler")
+ });
+ }));
+
+ Object.keys(VIEW_LABELS).forEach(function(view) {
+ var button = self.createButton(VIEW_LABELS[view],"tc-btn-invisible tc-task-board-view-button" + (view === currentView ? " tc-selected" : ""),function() {
+ self.setState(VIEW_STATE_TITLE,view);
+ });
+ button.setAttribute("aria-pressed",view === currentView ? "true" : "false");
+ switcher.appendChild(button);
+ });
+ toolbar.appendChild(switcher);
+ this.containerNode.appendChild(toolbar);
+};
+
+TaskBoardWidget.prototype.renderPriority = function(task) {
+ var badge = this.createElement("span","tc-task-priority tc-task-priority-" + task.effectivePriority.toLowerCase(),task.priority || "未设置");
+ badge.title = "优先级";
+ return badge;
+};
+
+TaskBoardWidget.prototype.renderIssues = function(task,parent) {
+ if(task.issues.length) {
+ var issue = this.createElement("span","tc-task-issues","⚠ " + task.issues.join(";"));
+ issue.title = task.issues.join(";");
+ parent.appendChild(issue);
+ }
+};
+
+TaskBoardWidget.prototype.renderTaskDates = function(task,parent) {
+ var values = [];
+ if(task.start) {values.push("开始 " + task.start);}
+ if(task.end) {values.push("结束 " + task.end);}
+ if(task.due) {values.push("截止 " + task.due);}
+ parent.appendChild(this.createElement("div","tc-task-card-dates",values.length ? values.join(" · ") : "未排期"));
+};
+
+TaskBoardWidget.prototype.updateTaskStatus = function(title,status) {
+ var tiddler = this.wiki.getTiddler(title);
+ if(tiddler && !tiddler.fields["draft.of"] && (tiddler.fields.tags || []).indexOf(this.tag) !== -1 && STATUSES.indexOf(status) !== -1) {
+ this.wiki.addTiddler(new $tw.Tiddler(tiddler,{status: status},this.wiki.getModificationFields()));
+ }
+};
+
+TaskBoardWidget.prototype.renderKanban = function(tasks) {
+ var self = this,
+ board = this.createElement("div","tc-task-kanban");
+
+ STATUSES.forEach(function(status) {
+ var column = self.createElement("section","tc-task-kanban-column tc-task-status-" + STATUSES.indexOf(status)),
+ columnTasks = tasks.filter(function(task) {return task.effectiveStatus === status;}),
+ heading = self.createElement("h2","tc-task-kanban-heading",status + " · " + columnTasks.length),
+ cards = self.createElement("div","tc-task-kanban-cards");
+
+ column.setAttribute("data-task-status",status);
+ column.addEventListener("dragover",function(event) {
+ event.preventDefault();
+ column.classList.add("tc-drag-over");
+ });
+ column.addEventListener("dragleave",function() {
+ column.classList.remove("tc-drag-over");
+ });
+ column.addEventListener("drop",function(event) {
+ event.preventDefault();
+ column.classList.remove("tc-drag-over");
+ var title = self.draggedTaskTitle;
+ if(title) {
+ self.updateTaskStatus(title,status);
+ }
+ self.draggedTaskTitle = null;
+ });
+
+ column.appendChild(heading);
+ columnTasks.forEach(function(task) {
+ var card = self.createElement("article","tc-task-card");
+ card.setAttribute("draggable","true");
+ card.setAttribute("data-task-title",task.title);
+ card.addEventListener("dragstart",function(event) {
+ self.draggedTaskTitle = task.title;
+ if(event.dataTransfer) {
+ event.dataTransfer.setData("application/x-tiddlywiki-task",task.title);
+ event.dataTransfer.effectAllowed = "move";
+ }
+ card.classList.add("tc-dragging");
+ });
+ card.addEventListener("dragend",function() {
+ self.draggedTaskTitle = null;
+ card.classList.remove("tc-dragging");
+ });
+ var cardTop = self.createElement("div","tc-task-card-top");
+ cardTop.appendChild(self.createTaskLink(task,"tc-task-card-title"));
+ cardTop.appendChild(self.renderPriority(task));
+ card.appendChild(cardTop);
+ self.renderTaskDates(task,card);
+ if(task.tags.length) {
+ card.appendChild(self.createElement("div","tc-task-card-tags",task.tags.join(" · ")));
+ }
+ self.renderIssues(task,card);
+ cards.appendChild(card);
+ });
+ if(!columnTasks.length) {
+ cards.appendChild(self.createElement("div","tc-task-empty-column","暂无事情"));
+ }
+ column.appendChild(cards);
+ board.appendChild(column);
+ });
+ this.containerNode.appendChild(board);
+};
+
+TaskBoardWidget.prototype.renderList = function(tasks) {
+ var self = this,
+ wrapper = this.createElement("div","tc-task-list-wrapper"),
+ table = this.createElement("table","tc-task-list"),
+ thead = this.createElement("thead"),
+ headerRow = this.createElement("tr"),
+ tbody = this.createElement("tbody");
+
+ ["事情","优先级","状态","开始","结束","截止"].forEach(function(label) {
+ headerRow.appendChild(self.createElement("th","",label));
+ });
+ thead.appendChild(headerRow);
+ table.appendChild(thead);
+
+ tasks.forEach(function(task) {
+ var row = self.createElement("tr","tc-task-list-row" + (task.issues.length ? " tc-task-invalid" : "")),
+ titleCell = self.createElement("td","tc-task-list-title");
+ titleCell.setAttribute("data-label","事情");
+ titleCell.appendChild(self.createTaskLink(task,"tc-task-list-link"));
+ self.renderIssues(task,titleCell);
+ row.appendChild(titleCell);
+ [
+ ["优先级",task.priority || "-"],
+ ["状态",task.status || "-"],
+ ["开始",task.start || "-"],
+ ["结束",task.end || "-"],
+ ["截止",task.due || "-"]
+ ].forEach(function(item) {
+ var cell = self.createElement("td","",item[1]);
+ cell.setAttribute("data-label",item[0]);
+ row.appendChild(cell);
+ });
+ tbody.appendChild(row);
+ });
+
+ if(!tasks.length) {
+ var emptyRow = this.createElement("tr"),
+ emptyCell = this.createElement("td","tc-task-empty","还没有事情");
+ emptyCell.setAttribute("colspan","6");
+ emptyRow.appendChild(emptyCell);
+ tbody.appendChild(emptyRow);
+ }
+
+ table.appendChild(tbody);
+ wrapper.appendChild(table);
+ this.containerNode.appendChild(wrapper);
+};
+
+TaskBoardWidget.prototype.renderMonthControls = function(value) {
+ var self = this,
+ controls = this.createElement("div","tc-task-calendar-controls"),
+ parts = value.split("-"),
+ year = Number(parts[0]),
+ month = Number(parts[1]);
+
+ function move(offset) {
+ self.setState(MONTH_STATE_TITLE,monthKey(new Date(Date.UTC(year,month - 1 + offset,1))));
+ }
+
+ controls.appendChild(this.createButton("上个月","tc-btn-invisible tc-task-month-button",function() {move(-1);}));
+ controls.appendChild(this.createElement("h2","tc-task-calendar-title",year + "年" + month + "月"));
+ controls.appendChild(this.createButton("本月","tc-btn-invisible tc-task-month-button",function() {
+ self.setState(MONTH_STATE_TITLE,monthKey(parseDate(todayKey())));
+ }));
+ controls.appendChild(this.createButton("下个月","tc-btn-invisible tc-task-month-button",function() {move(1);}));
+ this.containerNode.appendChild(controls);
+};
+
+TaskBoardWidget.prototype.renderCalendar = function(tasks) {
+ var self = this,
+ value = this.getMonth(),
+ parts = value.split("-"),
+ year = Number(parts[0]),
+ month = Number(parts[1]),
+ firstDate = new Date(Date.UTC(year,month - 1,1)),
+ firstWeekday = (firstDate.getUTCDay() + 6) % 7,
+ daysInMonth = new Date(Date.UTC(year,month,0)).getUTCDate(),
+ calendar = this.createElement("div","tc-task-calendar-grid"),
+ header = this.createElement("div","tc-task-calendar-header"),
+ weeks = this.createElement("div","tc-task-calendar-weeks"),
+ weekdays = ["一","二","三","四","五","六","日"],
+ unscheduled = tasks.filter(function(task) {
+ return !task.calendarDates.length || task.dateError || task.rangeError;
+ }),
+ today = todayKey(),
+ monthStart = firstDate,
+ monthEnd = new Date(Date.UTC(year,month - 1,daysInMonth)),
+ calendarStart = addDays(firstDate,-firstWeekday),
+ weekCount = Math.ceil((firstWeekday + daysInMonth) / 7);
+
+ this.renderMonthControls(value);
+ calendar.setAttribute("role","grid");
+ header.setAttribute("role","row");
+ weeks.setAttribute("role","rowgroup");
+ weekdays.forEach(function(weekday) {
+ var heading = self.createElement("div","tc-task-calendar-weekday",weekday);
+ heading.setAttribute("role","columnheader");
+ header.appendChild(heading);
+ });
+ calendar.appendChild(header);
+
+ for(var weekIndex = 0; weekIndex < weekCount; weekIndex++) {
+ var weekStart = addDays(calendarStart,weekIndex * 7),
+ weekEnd = addDays(weekStart,6),
+ week = this.createElement("div","tc-task-calendar-week"),
+ items = [],
+ lanes = [];
+ week.setAttribute("role","row");
+
+ tasks.forEach(function(task) {
+ if(task.calendarDates.length) {
+ var firstCalendarDate = task.calendarDates[0].date,
+ lastCalendarDate = task.calendarDates[task.calendarDates.length - 1].date,
+ segmentStart = new Date(Math.max(firstCalendarDate.getTime(),monthStart.getTime(),weekStart.getTime())),
+ segmentEnd = new Date(Math.min(lastCalendarDate.getTime(),monthEnd.getTime(),weekEnd.getTime()));
+ if(segmentStart <= segmentEnd) {
+ items.push({
+ task: task,
+ type: task.calendarDates.length === 1 ? "point" : "range",
+ start: dayNumber(segmentStart) - dayNumber(weekStart) + 1,
+ end: dayNumber(segmentEnd) - dayNumber(weekStart) + 1,
+ starts: dateKey(segmentStart) === dateKey(firstCalendarDate),
+ ends: dateKey(segmentEnd) === dateKey(lastCalendarDate),
+ dateLabel: task.calendarDates[0].label
+ });
+ }
+ if(task.calendarDates.length === 3) {
+ var middle = task.calendarDates[1];
+ if(middle.date >= monthStart && middle.date <= monthEnd && middle.date >= weekStart && middle.date <= weekEnd) {
+ var middleColumn = dayNumber(middle.date) - dayNumber(weekStart) + 1;
+ items.push({task: task,type: "milestone",start: middleColumn,end: middleColumn,dateLabel: middle.label});
+ }
+ }
+ }
+ });
+
+ items.forEach(function(item) {
+ var lane = 0;
+ while(lanes[lane] && lanes[lane].some(function(occupied) {
+ return item.start <= occupied.end && item.end >= occupied.start;
+ })) {
+ lane++;
+ }
+ lanes[lane] = lanes[lane] || [];
+ lanes[lane].push(item);
+ item.lane = lane;
+ });
+
+ week.style.gridTemplateRows = "minmax(2.25rem,1fr) repeat(" + lanes.length + ",1.5rem)";
+ for(var column = 0; column < 7; column++) {
+ var cellDate = addDays(weekStart,column),
+ inMonth = cellDate >= monthStart && cellDate <= monthEnd,
+ key = dateKey(cellDate),
+ cell = self.createElement("div","tc-task-calendar-day" + (!inMonth ? " tc-task-calendar-blank" : "") + (key === today ? " tc-task-calendar-today" : ""));
+ cell.style.gridColumn = (column + 1) + "";
+ cell.style.gridRow = "1 / span " + (lanes.length + 1);
+ cell.setAttribute("role","gridcell");
+ cell.setAttribute("aria-label",key);
+ if(inMonth) {
+ cell.appendChild(self.createElement("div","tc-task-calendar-day-number",cellDate.getUTCDate()));
+ }
+ week.appendChild(cell);
+ }
+
+ items.forEach(function(item) {
+ var className = item.type === "range" ? "tc-task-calendar-range" + (item.starts ? " tc-range-start" : "") + (item.ends ? " tc-range-end" : "") : item.type === "point" ? "tc-task-calendar-point" : "tc-task-calendar-milestone",
+ label = item.type === "point" || item.type === "milestone" ? item.dateLabel + " · " + item.task.title : item.task.title,
+ entry = self.createTaskLink(item.task,className,label);
+ entry.style.gridColumn = item.start + " / " + (item.end + 1);
+ entry.style.gridRow = (item.lane + 2) + "";
+ entry.setAttribute("data-priority",item.task.effectivePriority);
+ week.appendChild(entry);
+ });
+ weeks.appendChild(week);
+ }
+
+ calendar.appendChild(weeks);
+ this.containerNode.appendChild(calendar);
+ this.renderUnscheduled(unscheduled,"未排期或日期异常");
+};
+
+TaskBoardWidget.prototype.renderUnscheduled = function(tasks,label) {
+ var self = this;
+ if(!tasks.length) {
+ return;
+ }
+ var section = this.createElement("section","tc-task-unscheduled"),
+ list = this.createElement("div","tc-task-unscheduled-list");
+ section.appendChild(this.createElement("h3","",label + " · " + tasks.length));
+ tasks.forEach(function(task) {
+ list.appendChild(self.createTaskLink(task,"tc-task-unscheduled-link"));
+ });
+ section.appendChild(list);
+ this.containerNode.appendChild(section);
+};
+
+TaskBoardWidget.prototype.renderGantt = function(tasks) {
+ var self = this,
+ datedTasks = tasks.filter(function(task) {return task.rangeValid || (!task.start && task.endDate) || task.dueDate;}),
+ unscheduled = tasks.filter(function(task) {return task.rangeError || (!task.rangeValid && !(!task.start && task.endDate) && !task.dueDate);}),
+ dates = [];
+
+ datedTasks.forEach(function(task) {
+ var renderedDates = [];
+ if(task.rangeValid) {
+ renderedDates.push(task.startDate,task.endDate || task.startDate);
+ } else if(!task.start && task.endDate) {
+ renderedDates.push(task.endDate);
+ }
+ if(task.dueDate) {
+ renderedDates.push(task.dueDate);
+ }
+ renderedDates.forEach(function(date) {
+ if(date) {dates.push(dayNumber(date));}
+ });
+ });
+ if(!dates.length) {
+ this.containerNode.appendChild(this.createElement("div","tc-task-empty","没有可绘制的日期"));
+ this.renderUnscheduled(unscheduled,"未排期事情");
+ return;
+ }
+
+ var minimum = Math.min.apply(Math,dates) - 2,
+ maximum = Math.max.apply(Math,dates) + 2,
+ span = maximum - minimum + 1,
+ step = Math.max(1,Math.ceil(span / 366)),
+ units = Math.ceil(span / step),
+ labelReserve = Math.max.apply(Math,datedTasks.map(function(task) {
+ return task.title.length * 14 + 32;
+ }).concat([180])),
+ startDate = new Date(minimum * DAY_MS),
+ wrapper = this.createElement("div","tc-task-gantt-scroll"),
+ gantt = this.createElement("div","tc-task-gantt"),
+ header = this.createElement("div","tc-task-gantt-row tc-task-gantt-header"),
+ headerLabel = this.createElement("div","tc-task-gantt-label","事情"),
+ headerTimeline = this.createElement("div","tc-task-gantt-timeline tc-task-gantt-header-timeline"),
+ monthTrack = this.createElement("div","tc-task-gantt-months"),
+ dayTrack = this.createElement("div","tc-task-gantt-days"),
+ monthGroups = [],
+ today = parseDate(todayKey()),
+ todayOffset = (dayNumber(today) - minimum) / step;
+
+ gantt.style.setProperty("--task-gantt-width",units * DAY_WIDTH + labelReserve + "px");
+ gantt.style.setProperty("--task-gantt-date-width",units * DAY_WIDTH + "px");
+ header.appendChild(headerLabel);
+ for(var index = 0; index < units; index++) {
+ var date = addDays(startDate,index * step),
+ key = monthKey(date),
+ group = monthGroups[monthGroups.length - 1],
+ day = this.createElement("div","tc-task-gantt-day",pad(date.getUTCDate()));
+ if(!group || group.key !== key) {
+ group = {key: key,date: date,count: 0};
+ monthGroups.push(group);
+ }
+ group.count++;
+ day.style.width = DAY_WIDTH + "px";
+ day.setAttribute("title",dateKey(date) + (step > 1 ? " 起 " + step + " 天" : ""));
+ day.setAttribute("aria-label",dateKey(date) + (step > 1 ? " 起 " + step + " 天" : ""));
+ dayTrack.appendChild(day);
+ }
+ monthGroups.forEach(function(group) {
+ var segment = self.createElement("div","tc-task-gantt-month-segment"),
+ label = self.createElement("span","tc-task-gantt-month-label",group.date.getUTCFullYear() + "年" + (group.date.getUTCMonth() + 1) + "月");
+ segment.style.width = group.count * DAY_WIDTH + "px";
+ segment.setAttribute("title",label.textContent);
+ segment.appendChild(label);
+ monthTrack.appendChild(segment);
+ });
+ headerTimeline.appendChild(monthTrack);
+ headerTimeline.appendChild(dayTrack);
+ header.appendChild(headerTimeline);
+ gantt.appendChild(header);
+
+ datedTasks.forEach(function(task) {
+ var row = self.createElement("div","tc-task-gantt-row"),
+ label = self.createElement("div","tc-task-gantt-label"),
+ timeline = self.createElement("div","tc-task-gantt-timeline"),
+ barStart = task.startDate,
+ barEnd = task.rangeValid ? (task.endDate || task.startDate) : null;
+ label.appendChild(self.createTaskLink(task,"tc-task-gantt-task-link"));
+ label.appendChild(self.renderPriority(task));
+ self.renderIssues(task,label);
+
+ if(barStart && barEnd) {
+ var left = (dayNumber(barStart) - minimum) / step * DAY_WIDTH,
+ width = (dayNumber(barEnd) - dayNumber(barStart) + 1) / step * DAY_WIDTH,
+ barWidth = Math.max(width,DAY_WIDTH),
+ bar = self.createTaskLink(task,"tc-task-gantt-bar",""),
+ barLabel = self.createTaskLink(task,"tc-task-gantt-bar-label");
+ bar.style.left = left + "px";
+ bar.style.width = barWidth + "px";
+ bar.setAttribute("data-priority",task.effectivePriority);
+ barLabel.style.left = (left + barWidth + 8) + "px";
+ timeline.appendChild(bar);
+ timeline.appendChild(barLabel);
+ } else if(!task.start && task.endDate) {
+ var endMarker = self.createTaskLink(task,"tc-task-gantt-end","◆");
+ endMarker.style.left = ((dayNumber(task.endDate) - minimum) / step * DAY_WIDTH + DAY_WIDTH / 2) + "px";
+ endMarker.title = "计划结束:" + task.end;
+ timeline.appendChild(endMarker);
+ }
+ if(task.dueDate) {
+ var dueMarker = self.createTaskLink(task,"tc-task-gantt-due","◆");
+ dueMarker.style.left = ((dayNumber(task.dueDate) - minimum) / step * DAY_WIDTH + DAY_WIDTH / 2) + "px";
+ dueMarker.title = "截止:" + task.due;
+ timeline.appendChild(dueMarker);
+ }
+ if(todayOffset >= 0 && todayOffset < units) {
+ var todayLine = self.createElement("span","tc-task-gantt-today-line");
+ todayLine.style.left = (todayOffset * DAY_WIDTH + DAY_WIDTH / 2) + "px";
+ timeline.appendChild(todayLine);
+ }
+ row.appendChild(label);
+ row.appendChild(timeline);
+ gantt.appendChild(row);
+ });
+
+ wrapper.appendChild(gantt);
+ this.containerNode.appendChild(wrapper);
+ this.renderUnscheduled(unscheduled,"未排期事情");
+};
+
+TaskBoardWidget.prototype.refresh = function(changedTiddlers) {
+ var changedAttributes = this.computeAttributes();
+ if(Object.keys(changedAttributes).length || Object.keys(changedTiddlers).length) {
+ this.refreshSelf();
+ return true;
+ }
+ return false;
+};
+
+exports["task-board"] = TaskBoardWidget;
+
+})();
diff --git a/tiddlers/事情看板.tid b/tiddlers/事情看板.tid
new file mode 100644
index 0000000..4f4567f
--- /dev/null
+++ b/tiddlers/事情看板.tid
@@ -0,0 +1,6 @@
+created: 20260726132452393
+modified: 20260726132452393
+title: 事情看板
+type: text/vnd.tiddlywiki
+
+<$task-board tag="事情"/>