From dc290f4a8060dcd215415aa1585627e4205025d3 Mon Sep 17 00:00:00 2001 From: chenxi Date: Wed, 11 Nov 2026 11:03:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8C=89=E5=A4=A9=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=8C=96=E5=BE=85=E5=8A=9E=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Business/SystemizationTodoAuditLogger.php | 19 +++------- .../Business/SystemizationTodos.php | 22 +++++++---- .../pages/systemization-todos.blade.php | 33 +++++++++++++++-- .../Business/SystemizationTodosTest.php | 37 ++++++++++++++----- 4 files changed, 77 insertions(+), 34 deletions(-) diff --git a/app/Business/SystemizationTodoAuditLogger.php b/app/Business/SystemizationTodoAuditLogger.php index 87c612d0..d91de3cd 100644 --- a/app/Business/SystemizationTodoAuditLogger.php +++ b/app/Business/SystemizationTodoAuditLogger.php @@ -17,7 +17,7 @@ class SystemizationTodoAuditLogger public const ACTION_LABELS = [ 'create' => '新增', - 'update' => '修改', + 'update' => '改动', 'delete' => '删除', 'import' => '导入', ]; @@ -149,18 +149,6 @@ class SystemizationTodoAuditLogger } DB::table(self::TABLE)->insert($rows); - $companyIds = array_values(array_unique(array_column($rows, 'company_id'))); - foreach ($companyIds as $companyId) { - $maxId = (int) DB::table(self::TABLE) - ->where('company_id', $companyId) - ->max('id'); - if ($maxId > self::HISTORY_LIMIT) { - DB::table(self::TABLE) - ->where('company_id', $companyId) - ->where('id', '<=', $maxId - self::HISTORY_LIMIT) - ->delete(); - } - } } /** @@ -170,6 +158,7 @@ class SystemizationTodoAuditLogger int $companyId, int $limit = self::HISTORY_LIMIT, ?string $from = null, + ?string $date = null, ): array { if (! $this->tableReady()) { @@ -179,6 +168,10 @@ class SystemizationTodoAuditLogger return DB::table(self::TABLE) ->where('company_id', $companyId) ->when($from !== null && $from !== '', fn ($query) => $query->where('at', '>=', $from)) + ->when($date !== null && $date !== '', function ($query) use ($date): void { + $query->where('at', '>=', $date . ' 00:00:00') + ->where('at', '<', Carbon::parse($date)->addDay()->toDateString() . ' 00:00:00'); + }) ->orderByDesc('id') ->limit(max(1, min($limit, self::HISTORY_LIMIT))) ->get() diff --git a/app/Http/Controllers/Business/SystemizationTodos.php b/app/Http/Controllers/Business/SystemizationTodos.php index c6f5f3aa..e571b7fb 100644 --- a/app/Http/Controllers/Business/SystemizationTodos.php +++ b/app/Http/Controllers/Business/SystemizationTodos.php @@ -1509,20 +1509,21 @@ class SystemizationTodos extends Controller ]); } - public function history(): JsonResponse + public function history(Request $request): JsonResponse { - $from = $this->historyStartAt(); + $date = $this->historyDate($request); $events = $this->audit->history( (int) company_id(), SystemizationTodoAuditLogger::HISTORY_LIMIT, - $from, + null, + $date, ); return response()->json([ 'events' => $events, 'total' => count($events), 'limit' => SystemizationTodoAuditLogger::HISTORY_LIMIT, - 'from' => $from, + 'date' => $date, ]); } @@ -1569,12 +1570,13 @@ class SystemizationTodos extends Controller { $action = trim((string) $request->query('action', '')); $keyword = trim((string) $request->query('keyword', '')); - $from = $this->historyStartAt(); + $date = $this->historyDate($request); $rows = array_values(array_filter( $this->audit->history( (int) company_id(), SystemizationTodoAuditLogger::HISTORY_LIMIT, - $from, + null, + $date, ), fn (array $row): bool => $this->historyMatches($row, $action, $keyword), )); @@ -1601,9 +1603,13 @@ class SystemizationTodos extends Controller ]); } - private function historyStartAt(): string + private function historyDate(Request $request): string { - return $this->audit->platformNow()->startOfDay()->toDateTimeString(); + $data = $request->validate([ + 'date' => ['nullable', 'date_format:Y-m-d'], + ]); + + return trim((string) ($data['date'] ?? '')); } private function historyMatches(array $row, string $action, string $keyword): bool diff --git a/resources/views/business/pages/systemization-todos.blade.php b/resources/views/business/pages/systemization-todos.blade.php index e76eb571..7e641a84 100644 --- a/resources/views/business/pages/systemization-todos.blade.php +++ b/resources/views/business/pages/systemization-todos.blade.php @@ -295,6 +295,7 @@ .sys-todo-history-tools { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; margin-bottom:.75rem; } .sys-todo-history-tools select, .sys-todo-history-tools input { min-height:2.1rem; padding:0 .6rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; font:inherit; font-size:.78rem; } .sys-todo-history-tools input { flex:1 1 16rem; min-width:12rem; } + .sys-todo-history-tools input[type="date"] { flex:0 0 9.8rem; min-width:9.8rem; } .sys-todo-history-tools button { min-height:2.1rem; padding:0 .75rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; font:inherit; font-size:.78rem; cursor:pointer; } .sys-todo-history-tools button:hover { border-color:var(--sys-gold); background:var(--sys-gold-soft); } .sys-todo-history-meta { color:#666; font-size:.72rem; } @@ -311,6 +312,7 @@ .sys-todo-history-tag--create,.sys-todo-history-tag--import { border-color:#bad0ac; background:#f2f8ed; color:#53793e; } .sys-todo-history-tag--update { border-color:#dbbf7a; background:#fff7df; color:#935f07; } .sys-todo-history-tag--delete { border-color:#e7c3c3; background:#fff3f3; color:#b80000; } + .sys-todo-history-table__day td { padding:.46rem .65rem; border-bottom:1px solid #d8cbb7; background:#f8f2e7; color:#594a35; font-weight:600; } .sys-todo-history-item-title { margin-top:.2rem; color:#595959; font-size:.72rem; } .sys-todo-history-changes { margin:0; padding-left:1rem; } .sys-todo-history-changes li + li { margin-top:.22rem; } @@ -1091,14 +1093,15 @@ + - 从今天开始显示,时间为北京时间,最多显示最近 500 条 + 按北京时间记录并长期保留,可选择日期查看;单次最多显示 500 条
@@ -1421,6 +1424,7 @@ var groupNote = groupDialog.querySelector('[data-sys-todo-group-note]'); var historyDialog = document.getElementById('sysTodoHistoryDialog'); var historyAction = historyDialog.querySelector('[data-sys-todo-history-action]'); + var historyDate = historyDialog.querySelector('[data-sys-todo-history-date]'); var historyKeyword = historyDialog.querySelector('[data-sys-todo-history-keyword]'); var historyExport = historyDialog.querySelector('[data-sys-todo-history-export]'); var historyMeta = historyDialog.querySelector('[data-sys-todo-history-meta]'); @@ -2374,7 +2378,24 @@ function renderHistory() { var events = historyFilteredEvents(); historyBody.replaceChildren(); + var dayCounts = {}; events.forEach(function (event) { + var day = String(event.at || '').slice(0, 10); + dayCounts[day] = (dayCounts[day] || 0) + 1; + }); + var renderedDay = ''; + events.forEach(function (event) { + var day = String(event.at || '').slice(0, 10); + if (day !== renderedDay) { + renderedDay = day; + var dayRow = document.createElement('tr'); + dayRow.className = 'sys-todo-history-table__day'; + var dayCell = document.createElement('td'); + dayCell.colSpan = 5; + dayCell.textContent = (day || '日期未知') + ' · ' + dayCounts[day] + ' 条'; + dayRow.appendChild(dayCell); + historyBody.appendChild(dayRow); + } var row = document.createElement('tr'); var at = document.createElement('td'); @@ -2415,10 +2436,11 @@ historyEmpty.hidden = events.length > 0; historyEmpty.textContent = historyEvents.length ? '没有符合条件的操作记录。' - : '暂无操作记录;从本次功能上线后的第一次新增、修改、删除或导入开始记录。'; + : '暂无操作记录;从本次功能上线后的第一次新增、改动、删除或导入开始记录。'; historyMeta.textContent = '显示 ' + events.length + ' / ' + historyEvents.length + ' 条'; var params = new URLSearchParams(); if (historyAction.value) params.set('action', historyAction.value); + if (historyDate.value) params.set('date', historyDate.value); if (historyKeyword.value.trim()) params.set('keyword', historyKeyword.value.trim()); historyExport.setAttribute( 'data-export-url', @@ -2428,7 +2450,9 @@ function loadHistory() { historyMeta.textContent = '正在读取…'; - fetch(routes.history, { + var params = new URLSearchParams(); + if (historyDate.value) params.set('date', historyDate.value); + fetch(routes.history + (params.toString() ? '?' + params.toString() : ''), { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }).then(function (response) { return response.json().catch(function () { return {}; }).then(function (data) { @@ -2580,6 +2604,7 @@ if (event.target === historyDialog) closeHistoryDialog(); }); historyAction.addEventListener('change', renderHistory); + historyDate.addEventListener('change', loadHistory); historyKeyword.addEventListener('input', renderHistory); historyExport.addEventListener('click', function () { var url = historyExport.getAttribute('data-export-url'); diff --git a/tests/Feature/Business/SystemizationTodosTest.php b/tests/Feature/Business/SystemizationTodosTest.php index bf968074..ccaee28b 100644 --- a/tests/Feature/Business/SystemizationTodosTest.php +++ b/tests/Feature/Business/SystemizationTodosTest.php @@ -47,6 +47,9 @@ class SystemizationTodosTest extends FeatureTestCase ->assertSee('data-sys-todo-history-open', false) ->assertSee('id="sysTodoHistoryDialog"', false) ->assertSee('data-sys-todo-history-export', false) + ->assertSee('data-sys-todo-history-date', false) + ->assertSee('', false) + ->assertSeeText('按北京时间记录并长期保留') ->assertSee('data-sys-todo-inline-add', false) ->assertSee('data-sys-todo-col-resizer', false) ->assertSee('data-date-picker', false) @@ -2051,7 +2054,7 @@ class SystemizationTodosTest extends FeatureTestCase route('business.systemization-todos.history.export') . '?action=update' )->streamedContent(); $this->assertStringContainsString('时间,操作,类型,内容,待办事项,变更内容,操作人', $csv); - $this->assertStringContainsString('修改,收入,操作记录测试,修改后的待办', $csv); + $this->assertStringContainsString('改动,收入,操作记录测试,修改后的待办', $csv); $this->assertStringNotContainsString('删除前:', $csv); $this->assertStringNotContainsString('新增:', $csv); } @@ -2115,7 +2118,7 @@ class SystemizationTodosTest extends FeatureTestCase $this->assertSoftDeleted('business_systemization_todos', ['id' => $todo->id]); } - public function test_operation_history_starts_from_today(): void + public function test_operation_history_keeps_and_filters_records_across_days(): void { $this->loginAs(); @@ -2131,28 +2134,44 @@ class SystemizationTodosTest extends FeatureTestCase ])->assertRedirect(); $this->travelTo(Carbon::parse('2026-10-28 09:00:00', 'Asia/Shanghai')); - $this->post(route('business.systemization-todos.store'), [ + $todo = SystemizationTodo::query()->firstOrFail(); + $this->patch(route('business.systemization-todos.update', ['todo' => $todo->id]), [ 'category' => '收入', 'section' => '操作记录日期', - 'title' => '今天记录', + 'title' => '今天改动记录', 'owner' => '晨曦', 'assignee' => '栎阳', + 'start_date' => '2026-10-27', + 'due_date' => '2026-10-29', 'status' => SystemizationTodo::STATUS_INCOMPLETE, ])->assertRedirect(); $history = $this->getJson(route('business.systemization-todos.history')) ->assertOk() - ->assertJsonPath('from', '2026-10-28 00:00:00') + ->assertJsonPath('date', '') ->json(); $titles = array_column($history['events'], 'title'); - $this->assertContains('今天记录', $titles); - $this->assertNotContains('昨天记录', $titles); + $this->assertContains('今天改动记录', $titles); + $this->assertContains('昨天记录', $titles); + $this->assertSame(['改动', '新增'], array_column($history['events'], 'action_label')); + + $yesterday = $this->getJson(route('business.systemization-todos.history') . '?date=2026-10-27') + ->assertOk() + ->assertJsonPath('date', '2026-10-27') + ->json(); + $this->assertSame(['create'], array_column($yesterday['events'], 'action')); + $this->assertSame(['昨天记录'], array_column($yesterday['events'], 'title')); + + $this->withExceptionHandling() + ->getJson(route('business.systemization-todos.history') . '?date=2026-02-30') + ->assertUnprocessable(); $csv = $this->get(route('business.systemization-todos.history.export')) ->streamedContent(); - $this->assertStringContainsString('今天记录', $csv); - $this->assertStringNotContainsString('昨天记录', $csv); + $this->assertStringContainsString('昨天记录', $csv); + $this->assertStringContainsString('今天改动记录', $csv); + $this->assertStringContainsString('改动', $csv); } finally { $this->travelBack(); } -- 2.47.2