feat: unify business table interactions
This commit is contained in:
parent
0294a06b69
commit
44374faa0b
@ -175,6 +175,11 @@
|
||||
line-height: 1.35;
|
||||
cursor: pointer;
|
||||
}
|
||||
.directory-field-picker__option[draggable="true"] { cursor: grab; }
|
||||
.directory-field-picker__option.is-dragging { opacity: 0.55; }
|
||||
.directory-field-picker__option.is-drop-target { outline: 1px solid var(--vip-content-accent, #b9954f); background: var(--vip-content-accent-soft, #f7f0de); }
|
||||
.directory-field-picker__hint { grid-column: 1 / -1; color: var(--vip-content-muted, #595959); font-size: 0.6875rem; }
|
||||
.directory-table-tools .directory-export-group { display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
||||
.directory-field-picker__option input[type="checkbox"] {
|
||||
flex: 0 0 auto;
|
||||
width: 1rem;
|
||||
@ -485,16 +490,22 @@
|
||||
<div class="directory-field-picker" data-directory-field-picker>
|
||||
<button type="button" aria-expanded="false" aria-controls="directory-fields-{{ $page['slug'] ?? 'page' }}" data-directory-field-toggle>列设置</button>
|
||||
<div class="directory-field-picker__panel" id="directory-fields-{{ $page['slug'] ?? 'page' }}" role="group" aria-label="选择展示字段" hidden>
|
||||
<span class="directory-field-picker__hint">拖动字段调整顺序;固定序号列不可隐藏。</span>
|
||||
@foreach ($directoryFields as $field)
|
||||
<label class="directory-field-picker__option">
|
||||
<label class="directory-field-picker__option" draggable="true" data-directory-field-option>
|
||||
<input type="checkbox" value="{{ $field['name'] }}" data-directory-field-check @checked(!in_array($field['name'], ['sales_01', 'sales_02', 'sales_03', 'sales_04', 'sales_05', 'sales_06'], true))>
|
||||
<span>{{ $field['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" data-directory-export>导出当前页</button>
|
||||
<button type="button" data-directory-export-all>导出全部数据</button>
|
||||
<div class="directory-export-group" aria-label="导出范围">
|
||||
<button type="button" data-directory-export="visible">当前页·显示列</button>
|
||||
<button type="button" data-directory-export="all-fields">当前页·全部字段</button>
|
||||
<button type="button" data-directory-export-all="visible">全部结果·显示列</button>
|
||||
<button type="button" data-directory-export-all="all-fields">全部结果·全部字段</button>
|
||||
</div>
|
||||
<button type="button" data-directory-reset>恢复默认</button>
|
||||
<button type="button" data-directory-template>下载表头模板</button>
|
||||
</div>
|
||||
<div class="directory-table-scroll">
|
||||
@ -625,17 +636,37 @@
|
||||
section.dataset.directoryPage || 'page',
|
||||
section.dataset.directoryTab || 'tab',
|
||||
].join(':');
|
||||
const defaultOrder = checks.map((check) => check.value);
|
||||
const orderedChecks = () => [...(picker?.querySelectorAll('[data-directory-field-option]') || [])]
|
||||
.map((option) => option.querySelector('[data-directory-field-check]'))
|
||||
.filter(Boolean);
|
||||
const applyColumns = (save = true) => {
|
||||
const selected = new Set(checks.filter((check) => check.checked).map((check) => check.value));
|
||||
const ordered = orderedChecks();
|
||||
const selected = new Set(ordered.filter((check) => check.checked).map((check) => check.value));
|
||||
const rank = new Map(ordered.map((check, index) => [check.value, index]));
|
||||
table.dataset.columnsReady = 'true';
|
||||
table.querySelectorAll('[data-column-key]').forEach((cell) => {
|
||||
cell.classList.toggle('is-column-hidden', !selected.has(cell.dataset.columnKey));
|
||||
});
|
||||
if (save) localStorage.setItem(storageKey, JSON.stringify([...selected]));
|
||||
[...table.tHead.rows].forEach((row) => {
|
||||
[...row.children].sort((a, b) => (rank.get(a.dataset.columnKey) ?? -1) - (rank.get(b.dataset.columnKey) ?? -1))
|
||||
.forEach((cell) => row.appendChild(cell));
|
||||
});
|
||||
table.tBodies[0]?.querySelectorAll('tr').forEach((row) => {
|
||||
[...row.children].slice(1).sort((a, b) => (rank.get(a.dataset.columnKey) ?? -1) - (rank.get(b.dataset.columnKey) ?? -1))
|
||||
.forEach((cell) => row.appendChild(cell));
|
||||
});
|
||||
if (save) localStorage.setItem(storageKey, JSON.stringify({ order: ordered.map((check) => check.value), selected: [...selected] }));
|
||||
};
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || 'null');
|
||||
if (Array.isArray(saved)) checks.forEach((check) => { check.checked = saved.includes(check.value); });
|
||||
const savedOrder = Array.isArray(saved) ? saved : saved?.order;
|
||||
const savedSelected = Array.isArray(saved) ? saved : saved?.selected;
|
||||
if (Array.isArray(savedSelected)) checks.forEach((check) => { check.checked = savedSelected.includes(check.value); });
|
||||
if (Array.isArray(savedOrder)) savedOrder.forEach((key) => {
|
||||
const option = picker.querySelector(`[data-directory-field-option] input[value="${CSS.escape(key)}"]`)?.closest('[data-directory-field-option]');
|
||||
if (option) picker.querySelector('.directory-field-picker__panel').appendChild(option);
|
||||
});
|
||||
} catch (_) {}
|
||||
applyColumns(false);
|
||||
toggle?.addEventListener('click', () => {
|
||||
@ -644,28 +675,47 @@
|
||||
toggle.setAttribute('aria-expanded', String(isHidden));
|
||||
});
|
||||
checks.forEach((check) => check.addEventListener('change', () => applyColumns()));
|
||||
let dragging = null;
|
||||
picker?.querySelectorAll('[data-directory-field-option]').forEach((option) => {
|
||||
option.addEventListener('dragstart', () => { dragging = option; option.classList.add('is-dragging'); });
|
||||
option.addEventListener('dragend', () => { dragging = null; option.classList.remove('is-dragging'); picker.querySelectorAll('.is-drop-target').forEach((item) => item.classList.remove('is-drop-target')); applyColumns(); });
|
||||
option.addEventListener('dragover', (event) => { event.preventDefault(); if (dragging && dragging !== option) option.classList.add('is-drop-target'); });
|
||||
option.addEventListener('dragleave', () => option.classList.remove('is-drop-target'));
|
||||
option.addEventListener('drop', (event) => { event.preventDefault(); if (!dragging || dragging === option) return; option.parentNode.insertBefore(dragging, option); option.classList.remove('is-drop-target'); applyColumns(); });
|
||||
});
|
||||
const resetColumns = () => {
|
||||
const options = [...picker.querySelectorAll('[data-directory-field-option]')];
|
||||
const byKey = new Map(options.map((option) => [option.querySelector('[data-directory-field-check]').value, option]));
|
||||
defaultOrder.forEach((key) => picker.querySelector('.directory-field-picker__panel').appendChild(byKey.get(key)));
|
||||
checks.forEach((check) => { check.checked = !['sales_01', 'sales_02', 'sales_03', 'sales_04', 'sales_05', 'sales_06'].includes(check.value); });
|
||||
applyColumns();
|
||||
};
|
||||
const resetButton = tools.querySelector('[data-directory-reset]');
|
||||
if (resetButton) resetButton.onclick = resetColumns;
|
||||
search.addEventListener('input', () => {
|
||||
const query = search.value.trim().toLocaleLowerCase();
|
||||
table.querySelectorAll('tbody tr').forEach((row) => {
|
||||
row.hidden = Boolean(query) && !row.textContent.toLocaleLowerCase().includes(query);
|
||||
});
|
||||
});
|
||||
tools.querySelector('[data-directory-export]')?.addEventListener('click', () => {
|
||||
tools.querySelectorAll('[data-directory-export]').forEach((button) => button.addEventListener('click', () => {
|
||||
const selected = button.dataset.directoryExport === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
|
||||
const rows = [...table.querySelectorAll('tr')].filter((row) => !row.hidden);
|
||||
const csv = rows.map((row) => [...row.children].map((cell) => csvEscape((cell.querySelector('[data-password-status]') || cell).textContent.trim())).join(',')).join('\n');
|
||||
const cellsForExport = (row) => [...row.children].filter((cell) => !cell.dataset.columnKey || !selected || selected.has(cell.dataset.columnKey));
|
||||
const csv = rows.map((row) => cellsForExport(row).map((cell) => csvEscape((cell.querySelector('[data-password-status]') || cell).textContent.trim())).join(',')).join('\n');
|
||||
const blob = new Blob([`\ufeff${csv}`], { type: 'text/csv;charset=utf-8' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `${section.dataset.directoryPage || 'directory'}-export.csv`;
|
||||
link.download = `${section.dataset.directoryPage || 'directory'}-current-export.csv`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
});
|
||||
tools.querySelector('[data-directory-export-all]')?.addEventListener('click', async (event) => {
|
||||
}));
|
||||
tools.querySelectorAll('[data-directory-export-all]').forEach((button) => button.addEventListener('click', async (event) => {
|
||||
const button = event.currentTarget;
|
||||
const lastPage = Number(section.dataset.directoryLastPage || 1);
|
||||
const selected = new Set(checks.filter((check) => check.checked).map((check) => check.value));
|
||||
const selected = button.dataset.directoryExportAll === 'all-fields' ? null : new Set(orderedChecks().filter((check) => check.checked).map((check) => check.value));
|
||||
const cellsForExport = (row) => [...row.children]
|
||||
.filter((cell) => !cell.dataset.columnKey || selected.has(cell.dataset.columnKey));
|
||||
.filter((cell) => !cell.dataset.columnKey || !selected || selected.has(cell.dataset.columnKey));
|
||||
const header = cellsForExport(table.tHead.rows[0]).map((cell) => csvEscape(cell.textContent.trim())).join(',');
|
||||
const lines = [];
|
||||
button.disabled = true;
|
||||
@ -696,7 +746,7 @@
|
||||
button.disabled = false;
|
||||
button.textContent = originalLabel;
|
||||
}
|
||||
});
|
||||
}));
|
||||
tools.querySelector('[data-directory-template]')?.addEventListener('click', () => {
|
||||
const header = [...table.querySelectorAll('thead th')].map((cell) => csvEscape(cell.textContent.trim())).join(',');
|
||||
const blob = new Blob([`\ufeff${header}\n`], { type: 'text/csv;charset=utf-8' });
|
||||
|
||||
@ -533,6 +533,18 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
$response->assertSeeText($header);
|
||||
}
|
||||
}
|
||||
|
||||
$this->get(route('business.pages.show', [
|
||||
'section' => 'store-management',
|
||||
'page' => 'store-report',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSee('data-directory-field-picker', false)
|
||||
->assertSee('data-directory-export="visible"', false)
|
||||
->assertSee('data-directory-export-all="all-fields"', false)
|
||||
->assertSee('data-directory-reset', false)
|
||||
->assertSee('position: sticky', false)
|
||||
->assertSee('overflow: auto', false);
|
||||
}
|
||||
|
||||
private function mockUpstreamPage(object $row): void
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user