订单售后费用明细当前页导出
在订单售后费用明细筛选区增加当前页 CSV 导出,并覆盖空数据和 CSV 安全处理。
This commit is contained in:
parent
1993996a96
commit
2aec157a19
@ -167,6 +167,20 @@
|
||||
<span class="material-icons-outlined" aria-hidden="true">refresh</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
id="order-data-after-sales-export-button"
|
||||
data-export-contract="order-data.after-sales.current-page-export"
|
||||
data-export-table="order-data-after-sales-table"
|
||||
data-export-filename="订单售后费用明细_{{ $filters['from'] ?? '开始' }}_{{ $filters['to'] ?? '结束' }}_第{{ $rows?->currentPage() ?? 1 }}页.csv"
|
||||
@class(['upstream-data-button', 'is-disabled' => !$rows || $rows->isEmpty()])
|
||||
@if (!$rows || $rows->isEmpty()) disabled="disabled" aria-disabled="true" @endif
|
||||
aria-label="导出当前页"
|
||||
title="导出当前页"
|
||||
>
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
<span>导出当前页</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -179,7 +193,7 @@
|
||||
|
||||
<div class="upstream-data-table-panel">
|
||||
<div class="upstream-data-table-scroll">
|
||||
<table class="upstream-data-table order-data-table order-data-table--wide" aria-label="订单售后费用明细">
|
||||
<table id="order-data-after-sales-table" class="upstream-data-table order-data-table order-data-table--wide" aria-label="订单售后费用明细">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach ($columns as $field => $header)
|
||||
@ -192,7 +206,11 @@
|
||||
<tr>
|
||||
@foreach ($columns as $field => $header)
|
||||
@php $value = data_get($row, $field); @endphp
|
||||
<td @class(['is-number' => isset($numberFields[$field])])>
|
||||
<td
|
||||
data-export-value="{{ $value ?? '' }}"
|
||||
data-export-number="{{ isset($numberFields[$field]) ? 'true' : 'false' }}"
|
||||
@class(['is-number' => isset($numberFields[$field])])
|
||||
>
|
||||
@if ($field === 'product_link' && $value)
|
||||
<a class="order-data-link" href="{{ $value }}" target="_blank" rel="noopener" title="打开商品链接" aria-label="打开商品链接">
|
||||
<span class="material-icons-outlined" aria-hidden="true">open_in_new</span>
|
||||
@ -236,3 +254,51 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const button = document.getElementById('order-data-after-sales-export-button');
|
||||
|
||||
if (!button || button.dataset.exportBound === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
button.dataset.exportBound = 'true';
|
||||
button.addEventListener('click', () => {
|
||||
const table = document.getElementById(button.dataset.exportTable || '');
|
||||
const bodyRows = table ? Array.from(table.querySelectorAll('tbody tr:not(.is-empty)')) : [];
|
||||
|
||||
if (!table || bodyRows.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const protectSpreadsheetFormula = (value) => /^[=+\-@\t\r]/.test(value) ? `'${value}` : value;
|
||||
const csvCell = (value, isNumber = false) => {
|
||||
const raw = String(value ?? '').replace(/\r\n?/g, '\n');
|
||||
const normalized = isNumber ? raw : protectSpreadsheetFormula(raw);
|
||||
|
||||
return `"${normalized.replace(/"/g, '""')}"`;
|
||||
};
|
||||
const header = Array.from(table.querySelectorAll('thead th'))
|
||||
.map((cell) => csvCell(cell.textContent.trim()));
|
||||
const rows = bodyRows.map((row) => Array.from(row.querySelectorAll('td'))
|
||||
.map((cell) => csvCell(
|
||||
cell.dataset.exportValue ?? cell.textContent.trim(),
|
||||
cell.dataset.exportNumber === 'true',
|
||||
)));
|
||||
const csv = [header, ...rows].map((row) => row.join(',')).join('\r\n');
|
||||
const blob = new Blob(['\uFEFF', csv], { type: 'text/csv;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
|
||||
link.href = url;
|
||||
link.download = button.dataset.exportFilename || '订单售后费用明细.csv';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
window.setTimeout(() => {
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@ -46,19 +46,31 @@ class OrderDataPagesUpstreamTest extends FeatureTestCase
|
||||
$factory->shouldReceive('forSource')->twice()->with('operations_replica')->andReturn($cache);
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
|
||||
$this->loginAs()
|
||||
$afterSalesResponse = $this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'order-data',
|
||||
'page' => 'after-sales-expenses',
|
||||
'order_no' => 'ORDER-1',
|
||||
'from' => '2026-08-01',
|
||||
'to' => '2026-08-09',
|
||||
]))
|
||||
]));
|
||||
|
||||
$afterSalesResponse
|
||||
->assertOk()
|
||||
->assertSeeText('ORDER-1')
|
||||
->assertSeeText('测试店铺')
|
||||
->assertSeeText('123.45')
|
||||
->assertSeeText('DIRECT');
|
||||
->assertSeeText('DIRECT')
|
||||
->assertSeeText('导出当前页')
|
||||
->assertSee('id="order-data-after-sales-export-button"', false)
|
||||
->assertSee('data-export-contract="order-data.after-sales.current-page-export"', false)
|
||||
->assertSee('data-export-table="order-data-after-sales-table"', false)
|
||||
->assertSee('data-export-value="ORDER-1"', false)
|
||||
->assertSee('data-export-value="123.45"', false)
|
||||
->assertSee('data-export-number="true"', false)
|
||||
->assertSee('window.setTimeout(() => {', false)
|
||||
->assertSee('URL.revokeObjectURL(url);', false)
|
||||
->assertSee('订单售后费用明细_2026-08-01_2026-08-09_第1页.csv', false);
|
||||
|
||||
$this->get(route('business.pages.show', [
|
||||
'section' => 'order-data',
|
||||
@ -92,6 +104,29 @@ class OrderDataPagesUpstreamTest extends FeatureTestCase
|
||||
->assertSeeText('暂无数据');
|
||||
}
|
||||
|
||||
public function test_after_sales_export_is_disabled_when_the_current_page_has_no_rows(): void
|
||||
{
|
||||
$this->allowOrderDataPages();
|
||||
$cache = Mockery::mock(UpstreamQueryCache::class);
|
||||
$cache->shouldReceive('remember')->once()->andReturn(['rows' => []]);
|
||||
$cache->shouldReceive('responseHeaders')->once()->andReturn([
|
||||
'X-Caiwu-Data-Status' => 'direct',
|
||||
]);
|
||||
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
|
||||
$factory->shouldReceive('forSource')->once()->with('operations_replica')->andReturn($cache);
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'order-data',
|
||||
'page' => 'after-sales-expenses',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('导出当前页')
|
||||
->assertSee('id="order-data-after-sales-export-button"', false)
|
||||
->assertSee('disabled="disabled"', false);
|
||||
}
|
||||
|
||||
private function allowOrderDataPages(): void
|
||||
{
|
||||
$this->user->update(['locale' => 'zh-CN']);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user