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 . '',
+ ),
);
}