feat: complete task management workflow

This commit is contained in:
2026-09-05 11:18:52 +08:00
parent f182c704a4
commit 57145c198b
17 changed files with 1514 additions and 115 deletions
+108
View File
@@ -0,0 +1,108 @@
export type MinimalTask = {
id: string
title: string
description?: string
parent_id?: string | null
completed?: boolean
tags?: { name: string }[]
list_name?: string
}
const escapeHtml = (value: string) =>
value
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
const renderInline = (value: string) => {
let out = escapeHtml(value)
out = out.replace(/`([^`]+)`/g, '<code>$1</code>')
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>')
out = out.replace(
/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
)
return out
}
export function renderMarkdown(markdown = '') {
const lines = markdown.replace(/\r\n/g, '\n').split('\n')
const html: string[] = []
let inList = false
const closeList = () => {
if (inList) {
html.push('</ul>')
inList = false
}
}
for (const raw of lines) {
const line = raw.trimEnd()
if (!line.trim()) {
closeList()
continue
}
if (line.startsWith('# ')) {
closeList()
html.push(`<h1>${renderInline(line.slice(2))}</h1>`)
} else if (line.startsWith('## ')) {
closeList()
html.push(`<h2>${renderInline(line.slice(3))}</h2>`)
} else if (/^[-*] /.test(line)) {
if (!inList) {
html.push('<ul>')
inList = true
}
html.push(`<li>${renderInline(line.slice(2))}</li>`)
} else {
closeList()
html.push(`<p>${renderInline(line)}</p>`)
}
}
closeList()
return html.join('')
}
export function filterTasks<T extends MinimalTask>(tasks: T[], query: string) {
const q = query.trim().toLowerCase()
if (!q) return tasks
return tasks.filter((task) => {
const haystack = [
task.title,
task.description ?? '',
task.list_name ?? '',
...(task.tags ?? []).map((tag) => tag.name),
].join(' ')
return haystack.toLowerCase().includes(q)
})
}
export function groupTaskTree<T extends MinimalTask>(tasks: T[]) {
const children = new Map<string, T[]>()
for (const task of tasks) {
if (!task.parent_id) continue
children.set(task.parent_id, [...(children.get(task.parent_id) ?? []), task])
}
return tasks.filter((task) => !task.parent_id).map((task) => ({ task, subtasks: children.get(task.id) ?? [] }))
}
export function toDateTimeLocal(value: string | null | undefined) {
if (!value) return ''
const date = new Date(value)
if (Number.isNaN(date.valueOf())) return ''
const year = date.getFullYear()
const month = `${date.getMonth() + 1}`.padStart(2, '0')
const day = `${date.getDate()}`.padStart(2, '0')
const hour = `${date.getHours()}`.padStart(2, '0')
const minute = `${date.getMinutes()}`.padStart(2, '0')
return `${year}-${month}-${day}T${hour}:${minute}`
}
export function fromDateTimeLocal(value: string) {
if (!value) return null
return new Date(value).toISOString()
}