From 73a975434f5e258500e774d2516363c8088b69dd Mon Sep 17 00:00:00 2001 From: yueyang <2495544247@qq.com> Date: Sun, 25 Oct 2026 23:03:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=B3=BB=E7=BB=9F=E5=8C=96?= =?UTF-8?q?=E5=BE=85=E5=8A=9E=E9=80=BE=E6=9C=9F=E4=B8=8E=E5=A4=9A=E9=80=89?= =?UTF-8?q?=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Business/SystemizationTodos.php | 52 +++++++++++-- .../pages/systemization-todos.blade.php | 77 ++++++++++++++++--- .../Business/SystemizationTodosTest.php | 56 +++++++++++--- 3 files changed, 158 insertions(+), 27 deletions(-) diff --git a/app/Http/Controllers/Business/SystemizationTodos.php b/app/Http/Controllers/Business/SystemizationTodos.php index 023a43de..2b300249 100644 --- a/app/Http/Controllers/Business/SystemizationTodos.php +++ b/app/Http/Controllers/Business/SystemizationTodos.php @@ -17,6 +17,8 @@ use Throwable; class SystemizationTodos extends Controller { + private const OVERDUE_FILTER = '__overdue__'; + public function page(): array { $filters = $this->filters(); @@ -24,7 +26,11 @@ class SystemizationTodos extends Controller ->with('attachments') ->withCount('attachments'); - if ($filters['status'] !== '') { + if ($filters['status'] === self::OVERDUE_FILTER) { + $query->whereNotIn('status', [SystemizationTodo::STATUS_COMPLETED]) + ->whereNotNull('due_date') + ->whereDate('due_date', '<', today()); + } elseif ($filters['status'] !== '') { $query->where('status', $filters['status']); } if ($filters['category'] !== '') { @@ -33,8 +39,16 @@ class SystemizationTodos extends Controller if ($filters['owner'] !== '') { $query->where('owner', 'like', '%' . addcslashes($filters['owner'], '%_\\') . '%'); } - if ($filters['assignee'] !== '') { - $query->where('assignee', 'like', '%' . addcslashes($filters['assignee'], '%_\\') . '%'); + if ($filters['assignees'] !== []) { + $query->where(function ($builder) use ($filters): void { + foreach ($filters['assignees'] as $assignee) { + $builder->orWhere( + 'assignee', + 'like', + '%' . addcslashes($assignee, '%_\\') . '%', + ); + } + }); } if ($filters['start_date'] !== '') { $query->whereDate('start_date', $filters['start_date']); @@ -56,6 +70,20 @@ class SystemizationTodos extends Controller } $items = $query + ->select($query->qualifyColumn('*')) + ->selectRaw( + "ROW_NUMBER() OVER ( + PARTITION BY COALESCE(NULLIF(category, ''), '__uncategorized__') + ORDER BY + CASE WHEN status = '已完成' THEN 1 ELSE 0 END, + CASE WHEN section IS NULL OR section = '' THEN 1 ELSE 0 END, + section, + CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, + due_date, + sort_order, + id + ) AS category_sequence" + ) ->orderByRaw("CASE WHEN status = '已完成' THEN 1 ELSE 0 END") ->orderByRaw("CASE WHEN category IS NULL OR category = '' THEN 1 ELSE 0 END") ->orderBy('category') @@ -85,6 +113,7 @@ class SystemizationTodos extends Controller 'todoOwners' => $this->peopleOptions('owner'), 'todoAssignees' => $this->peopleOptions('assignee'), 'todoStatuses' => SystemizationTodo::STATUSES, + 'todoOverdueFilter' => self::OVERDUE_FILTER, 'todoSummary' => [ 'total' => (clone $base)->count(), 'pending' => (clone $base)->where('status', SystemizationTodo::STATUS_PENDING)->count(), @@ -204,13 +233,24 @@ class SystemizationTodos extends Controller private function filters(): array { + $status = (string) request('status', ''); + $assignees = collect((array) request('assignee', [])) + ->filter(static fn (mixed $value): bool => is_string($value)) + ->map(static fn (string $value): string => trim($value)) + ->filter() + ->unique() + ->take(50) + ->values() + ->all(); + return [ - 'status' => in_array(request('status'), SystemizationTodo::STATUSES, true) - ? (string) request('status') + 'status' => $status === self::OVERDUE_FILTER + || in_array($status, SystemizationTodo::STATUSES, true) + ? $status : '', 'category' => trim((string) request('category', '')), 'owner' => trim((string) request('owner', '')), - 'assignee' => trim((string) request('assignee', '')), + 'assignees' => $assignees, 'start_date' => $this->normalizeDateFilter(request('start_date')), 'due_date' => $this->normalizeDateFilter(request('due_date')), 'keyword' => trim((string) request('keyword', '')), diff --git a/resources/views/business/pages/systemization-todos.blade.php b/resources/views/business/pages/systemization-todos.blade.php index 687c4fe7..ef6d4feb 100644 --- a/resources/views/business/pages/systemization-todos.blade.php +++ b/resources/views/business/pages/systemization-todos.blade.php @@ -21,6 +21,19 @@ .sys-todo-filter label { display:grid; flex:1 1 7.4rem; gap:.25rem; min-width:0; color:#595959; font-size:.72rem; } .sys-todo-filter label.sys-todo-filter__keyword { flex:2.2 1 16rem; } .sys-todo-filter input, .sys-todo-filter select { width:100%; height:2.125rem; padding:0 .6rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; font:inherit; font-size:.8125rem; } + .sys-todo-filter__multi { position:relative; flex:1 1 8.2rem; min-width:0; color:#595959; font-size:.72rem; } + .sys-todo-filter__multi > summary { position:relative; display:grid; align-content:center; gap:.18rem; min-height:2.95rem; padding:.28rem 1.8rem .28rem .6rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; cursor:pointer; list-style:none; } + .sys-todo-filter__multi > summary::-webkit-details-marker { display:none; } + .sys-todo-filter__multi > summary .material-icons-outlined { position:absolute; top:50%; right:.55rem; color:#777; font-size:1.05rem; transform:translateY(-50%); transition:transform 150ms ease; } + .sys-todo-filter__multi[open] > summary { border-color:var(--sys-gold); box-shadow:0 0 0 3px rgba(133,102,50,.14); } + .sys-todo-filter__multi[open] > summary .material-icons-outlined { transform:translateY(-50%) rotate(180deg); } + .sys-todo-filter__multi-label { color:#595959; font-size:.72rem; line-height:1; } + .sys-todo-filter__multi-value { overflow:hidden; font-size:.8125rem; line-height:1.2; text-overflow:ellipsis; white-space:nowrap; } + .sys-todo-filter__multi-menu { position:absolute; top:calc(100% + .3rem); left:0; z-index:20; display:grid; width:max(100%,13rem); max-height:17rem; padding:.35rem; border:1px solid var(--sys-border); border-radius:.55rem; background:#fff; box-shadow:0 8px 22px rgba(23,23,23,.16); overflow:auto; } + .sys-todo-filter__multi-menu label { display:flex; flex:none; align-items:center; gap:.45rem; min-height:2rem; padding:.3rem .45rem; border-radius:.35rem; color:#333; font-size:.78rem; cursor:pointer; } + .sys-todo-filter__multi-menu label:hover { background:var(--sys-gold-soft); } + .sys-todo-filter__multi-menu input[type="checkbox"] { flex:0 0 auto; width:1rem; height:1rem; padding:0; accent-color:var(--sys-gold); } + .sys-todo-filter__multi-empty { padding:.65rem; color:#777; font-size:.75rem; text-align:center; } .sys-todo-filter__actions { display:flex; flex:0 0 auto; gap:.45rem; } .sys-todo-table-wrap { overflow:auto; border:1px solid var(--sys-border); border-radius:.65rem; background:#fff; } .sys-todo-table-wrap { max-height:clamp(24rem,58vh,46rem); overscroll-behavior:contain; scrollbar-gutter:stable; } @@ -156,6 +169,7 @@
@php - $cleanSectionLabel = static function (?string $value): string { + $cleanDisplayLabel = static function (?string $value): string { return trim((string) preg_replace( '/^\s*(?:\d+|[一二三四五六七八九十百]+)\s*[、..::)]\s*/u', '', @@ -196,7 +229,10 @@ )); }; $sectionLabels = $items - ->map(static fn ($groupItem): string => $cleanSectionLabel($groupItem->section)) + ->map(static fn ($groupItem): string => $cleanDisplayLabel($groupItem->section)) + ->all(); + $titleLabels = $items + ->map(static fn ($groupItem): string => $cleanDisplayLabel($groupItem->title)) ->all(); $groupValues = [ 'category' => $items->map(static fn ($groupItem): string => trim((string) ($groupItem->category ?? '')))->all(), @@ -264,7 +300,7 @@ 'id' => $item->id, 'category' => $item->category ?? '', 'section' => $sectionLabels[$loop->index] ?? '', - 'title' => $item->title, + 'title' => $titleLabels[$loop->index] ?? '', 'owner' => $item->owner ?? '', 'assignee' => $item->assignee ?? '', 'start_date' => $item->start_date?->format('Y-m-d') ?? '', @@ -295,8 +331,8 @@