From 38a765fa47fc0892b02db68f736beee5ae874f09 Mon Sep 17 00:00:00 2001 From: yueyang <2495544247@qq.com> Date: Mon, 26 Oct 2026 18:48:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=85=E5=8A=9E=E5=86=85=E5=AE=B9=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=80=89=E6=8B=A9=E6=88=96=E6=96=B0=E5=A2=9E=20(#631)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Business/SystemizationTodos.php | 25 ++++ .../pages/systemization-todos.blade.php | 108 +++++++++++++++++- .../Business/SystemizationTodosTest.php | 9 +- 3 files changed, 135 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Business/SystemizationTodos.php b/app/Http/Controllers/Business/SystemizationTodos.php index e1538461..4905f40d 100644 --- a/app/Http/Controllers/Business/SystemizationTodos.php +++ b/app/Http/Controllers/Business/SystemizationTodos.php @@ -146,6 +146,7 @@ class SystemizationTodos extends Controller 'todoPositionGroups' => $positionGroups, 'todoFilters' => $filters, 'todoCategories' => $categories, + 'todoContentOptions' => $this->contentOptions(), 'todoOwners' => $this->peopleOptions('owner'), 'todoAssignees' => $this->peopleOptions('assignee'), 'todoStatuses' => SystemizationTodo::STATUSES, @@ -636,6 +637,30 @@ class SystemizationTodos extends Controller ->values(); } + private function contentOptions(): array + { + $options = []; + $todos = SystemizationTodo::query() + ->whereNotNull('section') + ->where('section', '<>', '') + ->get(['category', 'section']); + foreach ($todos as $todo) { + $category = trim((string) ($todo->category ?? '')); + $section = $this->hierarchyLabel($todo->section); + if ($section !== '') { + $options[$category][$section] = true; + } + } + + foreach ($options as $category => $sections) { + $labels = array_keys($sections); + usort($labels, 'strnatcasecmp'); + $options[$category] = $labels; + } + + return $options; + } + private function normalizeDateFilter(mixed $value): string { $value = trim((string) $value); diff --git a/resources/views/business/pages/systemization-todos.blade.php b/resources/views/business/pages/systemization-todos.blade.php index 380637b1..106ae6a7 100644 --- a/resources/views/business/pages/systemization-todos.blade.php +++ b/resources/views/business/pages/systemization-todos.blade.php @@ -486,7 +486,20 @@ - +
+ + + + +
@@ -601,7 +614,10 @@ var categoryValue = dialog.querySelector('[data-sys-todo-category-value]'); var newCategoryWrap = dialog.querySelector('[data-sys-todo-new-category]'); var newCategoryInput = dialog.querySelector('[data-sys-todo-new-category-input]'); - var sectionInput = dialog.querySelector('[name="section"]'); + var sectionSelect = dialog.querySelector('[data-sys-todo-section-select]'); + var sectionValue = dialog.querySelector('[data-sys-todo-section-value]'); + var newSectionWrap = dialog.querySelector('[data-sys-todo-new-section]'); + var newSectionInput = dialog.querySelector('[data-sys-todo-new-section-input]'); var positionField = dialog.querySelector('[data-sys-todo-position-field]'); var positionSelect = dialog.querySelector('[data-sys-todo-position]'); var ownerValue = dialog.querySelector('[data-sys-todo-owner-value]'); @@ -620,6 +636,7 @@ var groupNote = groupDialog.querySelector('[data-sys-todo-group-note]'); var routes = @json($todoRoutes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); var positionGroups = @json($todoPositionGroups, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + var contentOptions = @json($todoContentOptions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); var currentTodoId = null; var currentGroupTodoId = null; var currentTodoTitle = ''; @@ -664,7 +681,7 @@ function currentPositionGroup() { var category = normalizedGroupText(categoryValue.value); - var section = normalizedGroupText(sectionInput.value); + var section = normalizedGroupText(sectionValue.value); return positionGroups.find(function (group) { return normalizedGroupText(group.category) === category && normalizedGroupText(group.section) === section; @@ -794,6 +811,76 @@ : categorySelect.value; } + function availableSectionLabels() { + var category = categoryValue.value; + if (category !== '' && Array.isArray(contentOptions[category])) { + return contentOptions[category].slice(); + } + + var labels = Object.values(contentOptions || {}).flat(); + return Array.from(new Set(labels)).sort(function (left, right) { + return String(left).localeCompare(String(right), 'zh-CN'); + }); + } + + function setSectionSelection(value) { + var normalized = String(value || '').trim(); + var hasOption = Array.prototype.some.call( + sectionSelect.options, + function (option) { return option.value === normalized && normalized !== '__new__'; } + ); + + if (normalized !== '' && !hasOption) { + var custom = document.createElement('option'); + custom.value = normalized; + custom.textContent = normalized; + sectionSelect.insertBefore(custom, sectionSelect.lastElementChild); + hasOption = true; + } + + sectionSelect.value = normalized; + newSectionWrap.hidden = true; + newSectionInput.required = false; + newSectionInput.value = ''; + sectionValue.value = normalized; + } + + function refreshSectionOptions(selectedValue) { + var normalized = String(selectedValue || '').trim(); + var labels = availableSectionLabels(); + sectionSelect.replaceChildren(); + var placeholder = document.createElement('option'); + placeholder.value = ''; + placeholder.textContent = '请选择内容'; + sectionSelect.appendChild(placeholder); + labels.forEach(function (label) { + var option = document.createElement('option'); + option.value = label; + option.textContent = label; + sectionSelect.appendChild(option); + }); + if (normalized !== '' && labels.indexOf(normalized) === -1) { + var current = document.createElement('option'); + current.value = normalized; + current.textContent = normalized; + sectionSelect.appendChild(current); + } + var create = document.createElement('option'); + create.value = '__new__'; + create.textContent = '+ 新增内容'; + sectionSelect.appendChild(create); + setSectionSelection(normalized); + } + + function syncSectionValue() { + var creating = sectionSelect.value === '__new__'; + newSectionWrap.hidden = !creating; + newSectionInput.required = creating; + sectionValue.value = creating + ? newSectionInput.value.trim() + : sectionSelect.value; + } + function appendScreenshotFiles(files) { var images = Array.prototype.slice.call(files || []).filter(function (file) { return /^image\/(jpeg|png|webp)$/.test(file.type) @@ -866,7 +953,7 @@ deleteButton.hidden = !currentTodoId; if (payload) { setCategoryValue(payload.category); - setValue('section', payload.section); + refreshSectionOptions(payload.section); setValue('title', payload.title); setOwnerValue(payload.owner); setValue('assignee', payload.assignee); @@ -878,6 +965,7 @@ setValue('notes', payload.notes); } else { setCategoryValue(''); + refreshSectionOptions(''); setOwnerValue(''); } if (!currentTodoId && !(payload && payload.status)) { @@ -1016,18 +1104,26 @@ }); categorySelect.addEventListener('change', function () { syncCategoryValue(); + refreshSectionOptions(sectionValue.value); populatePositionOptions(); }); newCategoryInput.addEventListener('input', function () { if (categorySelect.value === '__new__') { syncCategoryValue(); + refreshSectionOptions(sectionValue.value); populatePositionOptions(); } }); - sectionInput.addEventListener('input', function () { - resizeTextarea(sectionInput); + sectionSelect.addEventListener('change', function () { + syncSectionValue(); populatePositionOptions(); }); + newSectionInput.addEventListener('input', function () { + if (sectionSelect.value === '__new__') { + syncSectionValue(); + populatePositionOptions(); + } + }); dialog.addEventListener('input', function (event) { if (event.target.matches('[data-sys-todo-auto-grow]')) resizeTextarea(event.target); }); diff --git a/tests/Feature/Business/SystemizationTodosTest.php b/tests/Feature/Business/SystemizationTodosTest.php index 739cabf5..4ac24d5c 100644 --- a/tests/Feature/Business/SystemizationTodosTest.php +++ b/tests/Feature/Business/SystemizationTodosTest.php @@ -40,6 +40,8 @@ class SystemizationTodosTest extends FeatureTestCase ->assertSee('name="assignee[]"', false) ->assertSee('data-sys-todo-category-select', false) ->assertSee('data-sys-todo-new-category-input', false) + ->assertSee('data-sys-todo-section-select', false) + ->assertSee('data-sys-todo-new-section-input', false) ->assertSee('data-sys-todo-position', false) ->assertSeeText('待办事项序号') ->assertDontSee('name="type_sequence"', false) @@ -49,6 +51,7 @@ class SystemizationTodosTest extends FeatureTestCase ->assertSee('id="sysTodoGroupDialog"', false) ->assertDontSee('sys-todo-table__clamp', false) ->assertSeeText('+ 新增类型') + ->assertSeeText('+ 新增内容') ->assertSee('data-sys-todo-choose-files', false) ->assertSee('data-sys-todo-open-delete', false) ->assertSee('id="sysTodoDeleteConfirm"', false) @@ -320,6 +323,7 @@ class SystemizationTodosTest extends FeatureTestCase ->assertSee('收入事项 A') ->assertSee('收入事项 B'); $pageContent = $pageResponse->viewData('content'); + $this->assertContains('平台收入核对', $pageContent['todoContentOptions']['收入']); $this->assertSame( ['__overdue__', '待办'], array_column($pageContent['todoStatusOptions']->all(), 'value'), @@ -455,7 +459,10 @@ class SystemizationTodosTest extends FeatureTestCase ); $this->assertSame( 1, - substr_count((string) $response->getContent(), '>' . $mergedSection . '<'), + substr_count( + (string) $response->getContent(), + 'sys-todo-table__group-text">' . $mergedSection . '', + ), ); }