新增运费差导入模板下载 (#209)
This commit is contained in:
parent
1751d8e062
commit
9846f21297
@ -8,6 +8,7 @@ use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class CourierPayableSummaries extends Controller
|
||||
@ -45,6 +46,26 @@ class CourierPayableSummaries extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function template(string $summaryPage): BinaryFileResponse
|
||||
{
|
||||
$this->authorizePage($summaryPage);
|
||||
abort_unless($summaryPage === 'freight-difference', 404);
|
||||
|
||||
$path = resource_path('templates/courier-payables/freight-difference-import-template.xlsx');
|
||||
abort_unless(is_file($path), 404);
|
||||
|
||||
$response = response()->download($path, 'freight_difference_import_template.xlsx', [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
|
||||
return $response->setContentDisposition(
|
||||
'attachment',
|
||||
'运费差导入模板.xlsx',
|
||||
'freight_difference_import_template.xlsx',
|
||||
);
|
||||
}
|
||||
|
||||
public function export(string $summaryPage): StreamedResponse|RedirectResponse
|
||||
{
|
||||
$this->authorizePage($summaryPage);
|
||||
|
||||
Binary file not shown.
@ -9,6 +9,12 @@
|
||||
@endif
|
||||
</div>
|
||||
<div class="courier-summary__actions">
|
||||
@if ($courierSummaryPage === 'freight-difference')
|
||||
<button type="button" class="courier-summary__button" data-courier-summary-import-open>
|
||||
<span class="material-icons-outlined" aria-hidden="true">upload_file</span>
|
||||
<span>导入</span>
|
||||
</button>
|
||||
@else
|
||||
<form method="POST" enctype="multipart/form-data" action="{{ route('business.courier-payable-summaries.import', ['summaryPage' => $courierSummaryPage]) }}">
|
||||
@csrf
|
||||
<input id="courier-summary-import-{{ $courierSummaryPage }}" type="file" name="import" accept=".xlsx,.xls,.csv" required hidden onchange="this.form.submit()">
|
||||
@ -17,6 +23,7 @@
|
||||
<span>导入</span>
|
||||
</label>
|
||||
</form>
|
||||
@endif
|
||||
<a
|
||||
href="{{ route('business.courier-payable-summaries.export', ['summaryPage' => $courierSummaryPage]) }}"
|
||||
@class(['courier-summary__button', 'is-disabled' => $courierSummaryRows->total() === 0])
|
||||
@ -77,6 +84,61 @@
|
||||
<span>{{ $courierSummaryRows->onEachSide(1)->links() }}</span>
|
||||
</footer>
|
||||
@endif
|
||||
|
||||
@if ($courierSummaryPage === 'freight-difference')
|
||||
<dialog class="courier-summary__import-dialog" data-courier-summary-import-dialog aria-labelledby="courier-summary-import-title">
|
||||
<form
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
action="{{ route('business.courier-payable-summaries.import', ['summaryPage' => $courierSummaryPage]) }}"
|
||||
data-courier-summary-import-form
|
||||
>
|
||||
@csrf
|
||||
<header class="courier-summary__import-header">
|
||||
<div>
|
||||
<strong id="courier-summary-import-title">导入运费差</strong>
|
||||
<span>支持 .xlsx、.xls、.csv,单个文件不超过 10 MB</span>
|
||||
</div>
|
||||
<button type="button" class="courier-summary__icon-button" data-courier-summary-import-close aria-label="关闭导入窗口" title="关闭">
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
</header>
|
||||
<div class="courier-summary__import-body">
|
||||
<div class="courier-summary__template-row">
|
||||
<span class="material-icons-outlined" aria-hidden="true">description</span>
|
||||
<div>
|
||||
<strong>运费差导入模板</strong>
|
||||
<span>.xlsx · 10 个字段</span>
|
||||
</div>
|
||||
<a class="courier-summary__button" href="{{ route('business.courier-payable-summaries.template', ['summaryPage' => $courierSummaryPage]) }}">
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
<span>下载模板</span>
|
||||
</a>
|
||||
</div>
|
||||
<label class="courier-summary__file-picker" for="courier-summary-import-{{ $courierSummaryPage }}" role="button" tabindex="0" data-courier-summary-file-picker>
|
||||
<span class="material-icons-outlined" aria-hidden="true">upload_file</span>
|
||||
<span data-courier-summary-file-name>选择填写完成的文件</span>
|
||||
</label>
|
||||
<input
|
||||
id="courier-summary-import-{{ $courierSummaryPage }}"
|
||||
type="file"
|
||||
name="import"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
required
|
||||
hidden
|
||||
data-courier-summary-file-input
|
||||
>
|
||||
</div>
|
||||
<footer class="courier-summary__import-footer">
|
||||
<button type="button" class="courier-summary__button" data-courier-summary-import-close>取消</button>
|
||||
<button type="submit" class="courier-summary__button is-primary" data-courier-summary-import-submit disabled>
|
||||
<span class="material-icons-outlined" aria-hidden="true">upload_file</span>
|
||||
<span>导入</span>
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</dialog>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@ -91,6 +153,9 @@
|
||||
.courier-summary__button:hover { background:var(--vip-content-accent-soft,#f7f0de); color:var(--vip-content-accent-dark,#856632); text-decoration:none; }
|
||||
.courier-summary__button:focus-visible { outline:0; box-shadow:0 0 0 3px rgba(133,102,50,.2); }
|
||||
.courier-summary__button .material-icons-outlined { font-size:17px; }
|
||||
.courier-summary__button.is-primary { border-color:var(--vip-content-accent-dark,#856632); background:var(--vip-content-accent-dark,#856632); color:#fff; }
|
||||
.courier-summary__button.is-primary:hover { background:#6a5129; color:#fff; }
|
||||
.courier-summary__button:disabled { cursor:not-allowed; opacity:.45; }
|
||||
.courier-summary__button.is-disabled { pointer-events:none; opacity:.45; }
|
||||
.courier-summary__notice { padding:10px 12px; border-bottom:1px solid var(--vip-border,#d8d2c7); font-size:.75rem; }
|
||||
.courier-summary__notice.is-success { background:#f3f8ef; color:var(--vip-success,#53793e); }
|
||||
@ -104,5 +169,63 @@
|
||||
.courier-summary__table td.courier-summary__index { position:sticky; background:#fff; }
|
||||
.courier-summary__empty td { height:160px; color:var(--vip-content-muted,#595959); text-align:center; }
|
||||
.courier-summary__pagination { display:flex; min-height:44px; align-items:center; justify-content:space-between; gap:16px; padding:6px 12px; color:var(--vip-content-muted,#595959); font-size:.75rem; }
|
||||
@media (max-width:700px) { .courier-summary__toolbar { align-items:flex-start; flex-direction:column; } .courier-summary__actions { width:100%; } }
|
||||
.courier-summary__import-dialog { width:min(520px,calc(100vw - 32px)); padding:0; overflow:hidden; border:1px solid var(--vip-border,#d8d2c7); border-radius:8px; background:#fff; color:var(--vip-content-ink,#171717); box-shadow:0 4px 8px rgba(23,23,23,.1); }
|
||||
.courier-summary__import-dialog::backdrop { background:rgba(5,5,6,.45); }
|
||||
.courier-summary__import-header { display:flex; min-height:60px; align-items:center; justify-content:space-between; gap:16px; padding:12px 16px; border-bottom:1px solid var(--vip-border,#d8d2c7); background:var(--vip-content-bg,#f8f6f1); }
|
||||
.courier-summary__import-header strong,.courier-summary__import-header span { display:block; }
|
||||
.courier-summary__import-header strong { font-size:.875rem; }
|
||||
.courier-summary__import-header div>span { margin-top:3px; color:var(--vip-content-muted,#595959); font-size:.75rem; }
|
||||
.courier-summary__icon-button { display:inline-flex; width:34px; height:34px; flex:0 0 34px; align-items:center; justify-content:center; padding:0; border:0; border-radius:4px; background:transparent; color:var(--vip-content-muted,#595959); cursor:pointer; }
|
||||
.courier-summary__icon-button:hover { background:var(--vip-content-accent-soft,#f7f0de); color:var(--vip-content-accent-dark,#856632); }
|
||||
.courier-summary__icon-button:focus-visible { outline:0; box-shadow:0 0 0 3px rgba(133,102,50,.2); }
|
||||
.courier-summary__import-body { display:grid; gap:12px; padding:16px; }
|
||||
.courier-summary__template-row { display:grid; grid-template-columns:34px minmax(0,1fr) auto; align-items:center; gap:10px; min-height:54px; padding:9px 10px; border:1px solid var(--vip-border,#d8d2c7); border-radius:8px; }
|
||||
.courier-summary__template-row>.material-icons-outlined { color:var(--vip-content-accent-dark,#856632); }
|
||||
.courier-summary__template-row strong,.courier-summary__template-row div>span { display:block; }
|
||||
.courier-summary__template-row strong { font-size:.8125rem; }
|
||||
.courier-summary__template-row div>span { margin-top:2px; color:var(--vip-content-muted,#595959); font-size:.75rem; }
|
||||
.courier-summary__file-picker { display:flex; min-height:68px; align-items:center; justify-content:center; gap:8px; padding:12px; border:1px dashed var(--vip-content-accent,#b9954f); border-radius:8px; background:var(--vip-content-accent-soft,#f7f0de); color:var(--vip-content-accent-dark,#856632); font-size:.8125rem; font-weight:500; cursor:pointer; }
|
||||
.courier-summary__file-picker:hover { border-color:var(--vip-content-accent-dark,#856632); }
|
||||
.courier-summary__file-picker:focus-within { box-shadow:0 0 0 3px rgba(133,102,50,.2); }
|
||||
.courier-summary__import-footer { display:flex; min-height:58px; align-items:center; justify-content:flex-end; gap:8px; padding:12px 16px; border-top:1px solid var(--vip-border,#d8d2c7); background:var(--vip-content-bg,#f8f6f1); }
|
||||
@media (max-width:700px) { .courier-summary__toolbar { align-items:flex-start; flex-direction:column; } .courier-summary__actions { width:100%; } .courier-summary__template-row { grid-template-columns:28px minmax(0,1fr); } .courier-summary__template-row .courier-summary__button { grid-column:1/-1; justify-content:center; } }
|
||||
</style>
|
||||
|
||||
@if ($courierSummaryPage === 'freight-difference')
|
||||
<script>
|
||||
(() => {
|
||||
const root = document.querySelector('[data-courier-payable-summary="freight-difference"]');
|
||||
if (!root) return;
|
||||
|
||||
const dialog = root.querySelector('[data-courier-summary-import-dialog]');
|
||||
const openButton = root.querySelector('[data-courier-summary-import-open]');
|
||||
const closeButtons = [...root.querySelectorAll('[data-courier-summary-import-close]')];
|
||||
const filePicker = root.querySelector('[data-courier-summary-file-picker]');
|
||||
const fileInput = root.querySelector('[data-courier-summary-file-input]');
|
||||
const fileName = root.querySelector('[data-courier-summary-file-name]');
|
||||
const submitButton = root.querySelector('[data-courier-summary-import-submit]');
|
||||
const form = root.querySelector('[data-courier-summary-import-form]');
|
||||
|
||||
const close = () => dialog.close();
|
||||
openButton.addEventListener('click', () => dialog.showModal());
|
||||
closeButtons.forEach(button => button.addEventListener('click', close));
|
||||
dialog.addEventListener('click', event => {
|
||||
if (event.target === dialog) close();
|
||||
});
|
||||
filePicker.addEventListener('keydown', event => {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||
event.preventDefault();
|
||||
fileInput.click();
|
||||
});
|
||||
fileInput.addEventListener('change', () => {
|
||||
const file = fileInput.files?.[0];
|
||||
fileName.textContent = file ? file.name : '选择填写完成的文件';
|
||||
submitButton.disabled = !file;
|
||||
});
|
||||
form.addEventListener('submit', () => {
|
||||
submitButton.disabled = true;
|
||||
submitButton.querySelector('span:last-child').textContent = '正在导入';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
|
||||
@ -200,6 +200,9 @@ Route::prefix('workspace/courier-payables/courier-surcharge')
|
||||
Route::post('workspace/courier-payables/{summaryPage}/summary-import', [CourierPayableSummaries::class, 'import'])
|
||||
->where('summaryPage', 'freight-difference|other-surcharge-summary')
|
||||
->name('business.courier-payable-summaries.import');
|
||||
Route::get('workspace/courier-payables/{summaryPage}/summary-template', [CourierPayableSummaries::class, 'template'])
|
||||
->where('summaryPage', 'freight-difference')
|
||||
->name('business.courier-payable-summaries.template');
|
||||
Route::get('workspace/courier-payables/{summaryPage}/summary-export', [CourierPayableSummaries::class, 'export'])
|
||||
->where('summaryPage', 'freight-difference|other-surcharge-summary')
|
||||
->name('business.courier-payable-summaries.export');
|
||||
|
||||
@ -6,6 +6,8 @@ use App\Business\CourierPayableSummaryStore;
|
||||
use App\Models\Auth\Permission;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class CourierPayableSummaryPagesTest extends FeatureTestCase
|
||||
@ -49,6 +51,134 @@ class CourierPayableSummaryPagesTest extends FeatureTestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_freight_difference_import_dialog_downloads_the_supplied_xlsx_template(): void
|
||||
{
|
||||
$pageResponse = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'courier-payables',
|
||||
'page' => 'freight-difference',
|
||||
]));
|
||||
|
||||
$pageResponse
|
||||
->assertOk()
|
||||
->assertSee('data-courier-summary-import-dialog', false)
|
||||
->assertSeeText('下载模板')
|
||||
->assertSee(route('business.courier-payable-summaries.template', [
|
||||
'summaryPage' => 'freight-difference',
|
||||
]), false);
|
||||
|
||||
$templateResponse = $this->loginAs()->get(route('business.courier-payable-summaries.template', [
|
||||
'summaryPage' => 'freight-difference',
|
||||
]));
|
||||
|
||||
$templateResponse
|
||||
->assertOk()
|
||||
->assertDownload('freight_difference_import_template.xlsx')
|
||||
->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
$this->assertStringContainsString(
|
||||
"filename*=utf-8''" . rawurlencode('运费差导入模板.xlsx'),
|
||||
(string) $templateResponse->headers->get('Content-Disposition'),
|
||||
);
|
||||
|
||||
$spreadsheet = IOFactory::load(resource_path('templates/courier-payables/freight-difference-import-template.xlsx'));
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$this->assertSame([
|
||||
'负责人',
|
||||
'快递公司',
|
||||
'快递单号',
|
||||
'店铺',
|
||||
'id',
|
||||
'条码',
|
||||
'交易单号',
|
||||
'差异(结算运费-提供成本运费)',
|
||||
'是否配件',
|
||||
'配件成品条码',
|
||||
], $worksheet->rangeToArray('A1:J1', null, false, false)[0]);
|
||||
$this->assertSame('FFFFC000', $worksheet->getStyle('D1')->getFill()->getStartColor()->getARGB());
|
||||
$this->assertSame('FFFFC000', $worksheet->getStyle('E1')->getFill()->getStartColor()->getARGB());
|
||||
$this->assertSame('FFFFC000', $worksheet->getStyle('G1')->getFill()->getStartColor()->getARGB());
|
||||
$this->assertSame('@', $worksheet->getStyle('E2')->getNumberFormat()->getFormatCode());
|
||||
$this->assertSame('@', $worksheet->getStyle('G2')->getNumberFormat()->getFormatCode());
|
||||
$this->assertSame('', (string) $worksheet->getCell('A2')->getValue());
|
||||
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function test_other_surcharge_page_does_not_offer_the_freight_difference_template(): void
|
||||
{
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'courier-payables',
|
||||
'page' => 'other-surcharge-summary',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertDontSeeText('下载模板')
|
||||
->assertDontSee('data-courier-summary-import-dialog', false);
|
||||
}
|
||||
|
||||
public function test_completed_freight_difference_template_preserves_long_identifiers_on_import(): void
|
||||
{
|
||||
$spreadsheet = IOFactory::load(resource_path('templates/courier-payables/freight-difference-import-template.xlsx'));
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
$worksheet->fromArray([
|
||||
'苏木',
|
||||
'测试快递',
|
||||
'YT7632543323146',
|
||||
'测试店铺',
|
||||
'855226246304',
|
||||
'DNZ0117123N1210V01',
|
||||
'3312228147193110069',
|
||||
0.1827,
|
||||
'成品',
|
||||
'',
|
||||
], null, 'A2');
|
||||
$worksheet->getCell('E2')->setValueExplicit('855226246304', DataType::TYPE_STRING);
|
||||
$worksheet->getCell('G2')->setValueExplicit('3312228147193110069', DataType::TYPE_STRING);
|
||||
|
||||
$path = tempnam(sys_get_temp_dir(), 'freight-template-');
|
||||
$this->assertNotFalse($path);
|
||||
$xlsxPath = $path . '.xlsx';
|
||||
|
||||
try {
|
||||
IOFactory::createWriter($spreadsheet, 'Xlsx')->save($xlsxPath);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
|
||||
$file = new UploadedFile(
|
||||
$xlsxPath,
|
||||
'运费差导入模板.xlsx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
null,
|
||||
true,
|
||||
);
|
||||
|
||||
$this->loginAs()->post(route('business.courier-payable-summaries.import', [
|
||||
'summaryPage' => 'freight-difference',
|
||||
]), ['import' => $file])
|
||||
->assertRedirect(route('business.pages.show', [
|
||||
'section' => 'courier-payables',
|
||||
'page' => 'freight-difference',
|
||||
]))
|
||||
->assertSessionHas('success', '导入完成:1 条数据,10 个字段。');
|
||||
|
||||
$snapshot = app(CourierPayableSummaryStore::class)->snapshot(
|
||||
(int) $this->company->id,
|
||||
'freight-difference',
|
||||
);
|
||||
$this->assertSame('855226246304', $snapshot['rows'][0][4]);
|
||||
$this->assertSame('3312228147193110069', $snapshot['rows'][0][6]);
|
||||
$this->assertSame('0.1827', $snapshot['rows'][0][7]);
|
||||
} finally {
|
||||
if (isset($spreadsheet)) {
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
if ($path !== false && is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
if (isset($xlsxPath) && is_file($xlsxPath)) {
|
||||
unlink($xlsxPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function test_import_replaces_the_page_snapshot_and_export_returns_all_rows(): void
|
||||
{
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
@ -104,5 +234,9 @@ class CourierPayableSummaryPagesTest extends FeatureTestCase
|
||||
$this->loginAs()->get(route('business.courier-payable-summaries.export', [
|
||||
'summaryPage' => 'freight-difference',
|
||||
]))->assertForbidden();
|
||||
|
||||
$this->loginAs()->get(route('business.courier-payable-summaries.template', [
|
||||
'summaryPage' => 'freight-difference',
|
||||
]))->assertForbidden();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user