系统化待办改为单页并优化删除 #574
@ -101,8 +101,7 @@ class SystemizationTodos extends Controller
|
||||
->orderBy('due_date')
|
||||
->orderBy('sort_order')
|
||||
->orderBy('id')
|
||||
->paginate(50)
|
||||
->withQueryString();
|
||||
->get();
|
||||
|
||||
$base = SystemizationTodo::query();
|
||||
$categories = SystemizationTodo::query()
|
||||
|
||||
@ -36,20 +36,16 @@
|
||||
.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; }
|
||||
.sys-todo-table { width:100%; min-width:80rem; border-collapse:separate; border-spacing:0; font-size:.75rem; }
|
||||
.sys-todo-table-wrap { overflow-x:auto; overflow-y:visible; border:1px solid var(--sys-border); border-radius:.65rem; background:#fff; scrollbar-gutter:stable; }
|
||||
.sys-todo-table { width:100%; min-width:74rem; border-collapse:separate; border-spacing:0; font-size:.75rem; }
|
||||
.sys-todo-table th { position:sticky; top:0; z-index:4; padding:.55rem .48rem; border-bottom:1px solid var(--sys-border); background:#f3ece0; color:#3d3a35; text-align:left; white-space:nowrap; }
|
||||
.sys-todo-table td { padding:.45rem .48rem; border-bottom:1px solid #eee7da; background:#fff; color:#2f2a23; vertical-align:top; }
|
||||
.sys-todo-table td { padding:.65rem .55rem; border-bottom:1px solid #eee7da; background:#fff; color:#2f2a23; line-height:1.55; vertical-align:top; }
|
||||
.sys-todo-table tbody tr { cursor:pointer; }
|
||||
.sys-todo-table tbody tr:hover td { background:#fdfbf6; }
|
||||
.sys-todo-table tbody tr:focus-visible { outline:2px solid var(--sys-gold); outline-offset:-2px; }
|
||||
.sys-todo-table__index-cell { width:3.6rem; min-width:3.6rem; text-align:center; }
|
||||
.sys-todo-table th:last-child, .sys-todo-table td:last-child { position:sticky; right:0; z-index:3; width:5.8rem; min-width:5.8rem; background:#fff; box-shadow:-1px 0 0 #e5ddcf; }
|
||||
.sys-todo-table th:last-child { z-index:6; background:#f3ece0; }
|
||||
.sys-todo-table tbody tr:hover td:last-child { background:#fdfbf6; }
|
||||
.sys-todo-table__index { color:#6d5630; font-weight:600; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
||||
.sys-todo-table__group { vertical-align:middle; background:#fcfaf5; font-weight:600; }
|
||||
.sys-todo-table__group { padding-top:.8rem; padding-bottom:.8rem; background:#fcfaf5; font-weight:600; line-height:1.65; vertical-align:middle; }
|
||||
.sys-todo-table__section { min-width:11rem; max-width:17rem; }
|
||||
.sys-todo-table__title, .sys-todo-table__progress, .sys-todo-table__notes { line-height:1.45; white-space:pre-line; overflow-wrap:anywhere; }
|
||||
.sys-todo-table__title { min-width:17rem; max-width:24rem; font-weight:600; }
|
||||
@ -65,17 +61,11 @@
|
||||
.sys-todo-shots { display:flex; flex-wrap:wrap; gap:.3rem; max-width:13rem; }
|
||||
.sys-todo-shots button { display:block; width:4.25rem; height:3.2rem; padding:0; border:1px solid #ddd4c4; border-radius:.35rem; overflow:hidden; background:#faf8f3; cursor:zoom-in; }
|
||||
.sys-todo-shots img { width:100%; height:100%; object-fit:cover; }
|
||||
.sys-todo-row-actions { display:flex; flex-wrap:wrap; gap:.35rem; }
|
||||
.sys-todo-row-actions button, .sys-todo-row-actions a { padding:.2rem .45rem; border:1px solid var(--sys-border); border-radius:.35rem; background:#fff; color:#51483c; font:inherit; font-size:.72rem; text-decoration:none; cursor:pointer; }
|
||||
.sys-todo-row-actions button:hover, .sys-todo-row-actions a:hover { border-color:#c7b687; background:var(--sys-gold-soft); }
|
||||
.sys-todo-table__add { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.65rem; margin-top:.6rem; padding:.55rem .7rem; border:1px dashed #d8cdb8; border-radius:.55rem; background:#fcfaf5; color:#666; font-size:.72rem; }
|
||||
.sys-todo-table__add button { display:inline-flex; align-items:center; gap:.25rem; min-height:2rem; padding:0 .7rem; border:1px solid var(--sys-border); border-radius:.4rem; background:#fff; color:#51483c; font:inherit; font-size:.76rem; cursor:pointer; }
|
||||
.sys-todo-table__add button:hover { border-color:#c7b687; background:var(--sys-gold-soft); }
|
||||
.sys-todo-table__add .material-icons-outlined { font-size:1rem; }
|
||||
.sys-todo-empty { padding:2rem 1rem; color:#777; text-align:center; }
|
||||
.sys-todo-pagination { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem; padding:.85rem 0 0; color:#595959; font-size:.75rem; }
|
||||
.sys-todo-pagination__actions { display:flex; gap:.5rem; }
|
||||
.sys-todo-pagination a, .sys-todo-pagination span { padding:.3rem .55rem; border:1px solid var(--sys-border); border-radius:.35rem; background:#fff; color:#171717; text-decoration:none; }
|
||||
.sys-todo-flash { margin:0 0 .85rem; padding:.65rem .85rem; border:1px solid #bad0ac; border-radius:.45rem; background:#f2f8ed; color:#53793e; font-size:.8125rem; }
|
||||
.sys-todo-flash--error { border-color:#e7c3c3; background:#fff3f3; color:#b80000; }
|
||||
.sys-todo-import { margin:0 0 1rem; border:1px solid #e6dfd2; border-radius:.65rem; background:#fcfaf5; }
|
||||
@ -118,6 +108,19 @@
|
||||
.sys-todo-dialog__foot { display:flex; justify-content:flex-end; gap:.5rem; padding:.75rem 1.25rem; border-top:1px solid #e8e0d2; background:#fcfaf5; }
|
||||
.sys-todo-dialog__foot button { min-height:2.1rem; padding:0 .9rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; font:inherit; font-size:.8125rem; cursor:pointer; }
|
||||
.sys-todo-dialog__foot button[type="submit"] { border-color:var(--sys-gold); background:var(--sys-gold); color:#fff; }
|
||||
.sys-todo-dialog__foot .sys-todo-dialog__delete { margin-right:auto; border-color:#e7c3c3; color:#b80000; }
|
||||
.sys-todo-dialog__foot .sys-todo-dialog__delete:hover { background:#fff3f3; }
|
||||
.sys-todo-dialog__foot .sys-todo-dialog__delete[hidden] { display:none; }
|
||||
.sys-todo-confirm { width:min(30rem,calc(100vw - 2rem)); padding:0; border:1px solid var(--sys-border); border-radius:.8rem; background:#fff; color:#171717; overflow:hidden; }
|
||||
.sys-todo-confirm::backdrop { background:rgba(23,23,23,.58); }
|
||||
.sys-todo-confirm__head { display:flex; align-items:center; gap:.55rem; padding:.9rem 1rem; border-bottom:1px solid #e8e0d2; background:#fff8f8; color:#b80000; font-weight:600; }
|
||||
.sys-todo-confirm__head .material-icons-outlined { font-size:1.15rem; }
|
||||
.sys-todo-confirm__body { padding:1rem; color:#444; font-size:.84rem; line-height:1.7; }
|
||||
.sys-todo-confirm__body strong { display:block; margin-top:.45rem; color:#171717; font-weight:600; }
|
||||
.sys-todo-confirm__actions { display:flex; justify-content:flex-end; gap:.5rem; padding:.75rem 1rem; border-top:1px solid #e8e0d2; background:#fcfaf5; }
|
||||
.sys-todo-confirm__actions button { min-height:2.1rem; padding:0 .9rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; font:inherit; font-size:.8125rem; cursor:pointer; }
|
||||
.sys-todo-confirm__actions .sys-todo-confirm__submit { border-color:#b80000; background:#b80000; color:#fff; }
|
||||
.sys-todo-confirm__actions .sys-todo-confirm__submit:hover { background:#920000; }
|
||||
.sys-todo-image-dialog { width:min(72rem,calc(100vw - 2rem)); max-height:92vh; padding:0; border:1px solid var(--sys-border); border-radius:.8rem; background:#111; color:#fff; overflow:hidden; }
|
||||
.sys-todo-image-dialog::backdrop { background:rgba(5,5,6,.78); }
|
||||
.sys-todo-image-dialog__head { display:flex; align-items:center; gap:1rem; padding:.65rem .85rem; background:#1d1d1f; }
|
||||
@ -129,7 +132,7 @@
|
||||
.sys-todo-image-dialog__foot a { color:#f0e4bf; font-size:.75rem; }
|
||||
@media (max-width:1300px) { .sys-todo-summary { grid-template-columns:repeat(3,minmax(0,1fr)); } }
|
||||
@media (max-width:900px) { .sys-todo-filter label, .sys-todo-filter label.sys-todo-filter__keyword { flex-basis:9rem; } .sys-todo-filter__actions { width:100%; } .sys-todo-filter__actions .sys-todo-page__button { flex:1; } }
|
||||
@media (max-width:720px) { .sys-todo-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } .sys-todo-filter label, .sys-todo-filter label.sys-todo-filter__keyword { flex-basis:10rem; } .sys-todo-field--3,.sys-todo-field--4,.sys-todo-field--6 { grid-column:span 12; } .sys-todo-table-wrap { max-height:65vh; } }
|
||||
@media (max-width:720px) { .sys-todo-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } .sys-todo-filter label, .sys-todo-filter label.sys-todo-filter__keyword { flex-basis:10rem; } .sys-todo-field--3,.sys-todo-field--4,.sys-todo-field--6 { grid-column:span 12; } }
|
||||
</style>
|
||||
|
||||
<div class="biz-card-head">
|
||||
@ -304,7 +307,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">类型</th><th scope="col">内容</th><th scope="col">序号</th><th scope="col">待办事项</th><th scope="col">总负责人</th><th scope="col">责任人</th>
|
||||
<th scope="col">开始时间</th><th scope="col">完成时间</th><th scope="col">状态</th><th scope="col">完成情况</th><th scope="col">完成截图</th><th scope="col">备注</th><th scope="col">操作</th>
|
||||
<th scope="col">开始时间</th><th scope="col">完成时间</th><th scope="col">状态</th><th scope="col">完成情况</th><th scope="col">完成截图</th><th scope="col">备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -386,26 +389,16 @@
|
||||
@endif
|
||||
</td>
|
||||
<td class="sys-todo-table__notes"><div class="sys-todo-table__clamp" title="{{ $item->notes ?: '—' }}">{{ $item->notes ?: '—' }}</div></td>
|
||||
<td>
|
||||
<div class="sys-todo-row-actions">
|
||||
<button type="button" data-sys-todo-edit="{{ $todoPayloadEncoded }}">编辑</button>
|
||||
<form method="POST" action="{{ route('business.systemization-todos.destroy', ['todo' => $item->id]) }}" onsubmit="return confirm('确定删除这条待办吗?');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit">删除</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="13"><div class="sys-todo-empty">当前筛选条件下没有待办事项。</div></td></tr>
|
||||
<tr><td colspan="12"><div class="sys-todo-empty">当前筛选条件下没有待办事项。</div></td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="sys-todo-table__add">
|
||||
<span>点击表内任意事项可直接编辑;表格在框内滚动,表头和操作列保持可见。</span>
|
||||
<span>共 {{ number_format($items->count()) }} 条,已全部显示。点击任意事项行可直接编辑。</span>
|
||||
<button type="button" data-sys-todo-create
|
||||
data-sys-todo-default-category="{{ $lastItem?->category ?? '' }}"
|
||||
data-sys-todo-default-section="{{ $lastSectionLabel }}">
|
||||
@ -413,24 +406,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if ($items->hasPages())
|
||||
<div class="sys-todo-pagination">
|
||||
<span>共 {{ number_format($items->total()) }} 条 · 第 {{ $items->currentPage() }} / {{ $items->lastPage() }} 页</span>
|
||||
<span class="sys-todo-pagination__actions">
|
||||
@if ($items->onFirstPage())
|
||||
<span>上一页</span>
|
||||
@else
|
||||
<a href="{{ $items->previousPageUrl() }}">上一页</a>
|
||||
@endif
|
||||
@if ($items->hasMorePages())
|
||||
<a href="{{ $items->nextPageUrl() }}">下一页</a>
|
||||
@else
|
||||
<span>下一页</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<dialog class="sys-todo-dialog" id="sysTodoDialog" aria-labelledby="sysTodoDialogTitle">
|
||||
<form method="POST" enctype="multipart/form-data" data-sys-todo-form>
|
||||
@csrf
|
||||
@ -486,12 +461,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="sys-todo-dialog__foot">
|
||||
<button type="button" class="sys-todo-dialog__delete" data-sys-todo-open-delete hidden>删除待办</button>
|
||||
<button type="button" data-sys-todo-close>取消</button>
|
||||
<button type="submit">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog class="sys-todo-confirm" id="sysTodoDeleteConfirm" aria-labelledby="sysTodoDeleteConfirmTitle">
|
||||
<form method="POST" data-sys-todo-delete-form>
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<div class="sys-todo-confirm__head">
|
||||
<span class="material-icons-outlined" aria-hidden="true">warning</span>
|
||||
<span id="sysTodoDeleteConfirmTitle">确认删除待办</span>
|
||||
</div>
|
||||
<div class="sys-todo-confirm__body">
|
||||
删除后这条待办及其完成截图将不再显示,操作无法撤销。
|
||||
<strong data-sys-todo-delete-title>当前待办</strong>
|
||||
</div>
|
||||
<div class="sys-todo-confirm__actions">
|
||||
<button type="button" data-sys-todo-delete-close>取消</button>
|
||||
<button type="submit" class="sys-todo-confirm__submit">确定删除</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog class="sys-todo-image-dialog" id="sysTodoImageViewer" aria-labelledby="sysTodoImageViewerTitle">
|
||||
<div class="sys-todo-image-dialog__head">
|
||||
<h2 id="sysTodoImageViewerTitle" data-sys-todo-image-title>完成截图预览</h2>
|
||||
@ -529,8 +524,13 @@
|
||||
var ownerOptionInputs = Array.prototype.slice.call(
|
||||
dialog.querySelectorAll('[data-sys-todo-owner-options] input[type="checkbox"]')
|
||||
);
|
||||
var deleteButton = dialog.querySelector('[data-sys-todo-open-delete]');
|
||||
var deleteConfirm = document.getElementById('sysTodoDeleteConfirm');
|
||||
var deleteForm = deleteConfirm.querySelector('[data-sys-todo-delete-form]');
|
||||
var deleteTitle = deleteConfirm.querySelector('[data-sys-todo-delete-title]');
|
||||
var routes = @json($todoRoutes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
var currentTodoId = null;
|
||||
var currentTodoTitle = '';
|
||||
var uploadPrompt = '点击选择、拖入此区域或直接粘贴,支持多张,每张不超过 10 MB。';
|
||||
var ownerFilter = document.querySelector('[data-sys-todo-owner-filter]');
|
||||
var ownerSummary = ownerFilter.querySelector('[data-sys-todo-owner-summary]');
|
||||
@ -661,10 +661,12 @@
|
||||
|
||||
function openDialog(payload) {
|
||||
currentTodoId = payload && payload.id ? payload.id : null;
|
||||
currentTodoTitle = payload && payload.title ? payload.title : '';
|
||||
form.reset();
|
||||
methodInput.value = currentTodoId ? 'PATCH' : 'POST';
|
||||
form.action = currentTodoId ? routes.update.replace('__TODO__', currentTodoId) : routes.store;
|
||||
title.textContent = currentTodoId ? '编辑待办' : '新增待办';
|
||||
deleteButton.hidden = !currentTodoId;
|
||||
if (payload) {
|
||||
setCategoryValue(payload.category);
|
||||
setValue('section', payload.section);
|
||||
@ -724,6 +726,19 @@
|
||||
else dialog.removeAttribute('open');
|
||||
}
|
||||
|
||||
function openDeleteConfirm() {
|
||||
if (!currentTodoId) return;
|
||||
deleteForm.action = routes.destroy.replace('__TODO__', currentTodoId);
|
||||
deleteTitle.textContent = currentTodoTitle || '当前待办';
|
||||
if (typeof deleteConfirm.showModal === 'function') deleteConfirm.showModal();
|
||||
else deleteConfirm.setAttribute('open', '');
|
||||
}
|
||||
|
||||
function closeDeleteConfirm() {
|
||||
if (typeof deleteConfirm.close === 'function') deleteConfirm.close();
|
||||
else deleteConfirm.removeAttribute('open');
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
var image = event.target.closest('[data-sys-todo-image-url]');
|
||||
if (image) {
|
||||
@ -742,9 +757,8 @@
|
||||
});
|
||||
return;
|
||||
}
|
||||
var edit = event.target.closest('[data-sys-todo-edit]');
|
||||
if (edit) {
|
||||
openDialog(decodePayload(edit.getAttribute('data-sys-todo-edit')));
|
||||
if (event.target.closest('[data-sys-todo-open-delete]')) {
|
||||
openDeleteConfirm();
|
||||
return;
|
||||
}
|
||||
var row = event.target.closest('[data-sys-todo-row-edit]');
|
||||
@ -753,6 +767,7 @@
|
||||
return;
|
||||
}
|
||||
if (event.target.closest('[data-sys-todo-close]')) closeDialog();
|
||||
if (event.target.closest('[data-sys-todo-delete-close]')) closeDeleteConfirm();
|
||||
if (event.target.closest('[data-sys-todo-image-close]')) closeImageViewer();
|
||||
if (event.target.closest('[data-sys-todo-choose-files]')) screenshotInput.click();
|
||||
});
|
||||
@ -772,6 +787,9 @@
|
||||
imageViewer.addEventListener('click', function (event) {
|
||||
if (event.target === imageViewer) closeImageViewer();
|
||||
});
|
||||
deleteConfirm.addEventListener('click', function (event) {
|
||||
if (event.target === deleteConfirm) closeDeleteConfirm();
|
||||
});
|
||||
categorySelect.addEventListener('change', syncCategoryValue);
|
||||
newCategoryInput.addEventListener('input', function () {
|
||||
if (categorySelect.value === '__new__') syncCategoryValue();
|
||||
|
||||
@ -41,6 +41,10 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
->assertSee('data-sys-todo-new-category-input', false)
|
||||
->assertSeeText('+ 新增类型')
|
||||
->assertSee('data-sys-todo-choose-files', false)
|
||||
->assertSee('data-sys-todo-open-delete', false)
|
||||
->assertSee('id="sysTodoDeleteConfirm"', false)
|
||||
->assertDontSee('<th scope="col">操作</th>', false)
|
||||
->assertDontSeeText('下一页')
|
||||
->assertSee('id="sysTodoImageViewer"', false);
|
||||
|
||||
$this->post(route('business.systemization-todos.store'), [
|
||||
@ -195,7 +199,7 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
['__overdue__', '待办'],
|
||||
array_column($pageContent['todoStatusOptions']->all(), 'value'),
|
||||
);
|
||||
$pageItems = $pageContent['items']->getCollection();
|
||||
$pageItems = $pageContent['items'];
|
||||
$this->assertSame(
|
||||
[1, 2],
|
||||
$pageItems->where('category', '收入')
|
||||
@ -258,6 +262,34 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function test_page_renders_all_todos_on_one_continuous_list(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
foreach (range(1, 55) as $index) {
|
||||
$this->createTodo(
|
||||
$index % 2 === 0 ? '收入' : '成本',
|
||||
'连续列表测试',
|
||||
'连续事项 ' . $index,
|
||||
'2026-11-30',
|
||||
$index,
|
||||
);
|
||||
}
|
||||
|
||||
$response = $this->get(route('business.pages.show', [
|
||||
'section' => 'other',
|
||||
'page' => 'systemization-todos',
|
||||
]))->assertOk();
|
||||
|
||||
$content = $response->viewData('content');
|
||||
$this->assertCount(55, $content['items']);
|
||||
$this->assertTrue($content['items']->pluck('title')->contains('连续事项 55'));
|
||||
$response->assertSee('连续事项 55')
|
||||
->assertSeeText('共 55 条,已全部显示')
|
||||
->assertDontSeeText('下一页')
|
||||
->assertDontSeeText('上一页');
|
||||
}
|
||||
|
||||
private function createTodo(
|
||||
string $category,
|
||||
string $section,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user