From 72dff27db9bcb12410230a7627421e94115a7cdf Mon Sep 17 00:00:00 2001
From: zhoushengchen <3057083327@qq.com>
Date: Tue, 22 Sep 2026 18:22:23 +0800
Subject: [PATCH 1/4] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BF=AB=E9=80=92?=
=?UTF-8?q?=E9=99=84=E5=8A=A0=E8=B4=B9=E4=BE=9B=E5=BA=94=E5=95=86=E5=AD=97?=
=?UTF-8?q?=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CourierSurchargeAuditLogRepository.php | 1 +
.../CourierSurchargeQuoteRepository.php | 18 ++++++-
.../CourierSurchargeQuoteWorkbookImporter.php | 14 ++++--
.../Business/CourierSurchargeQuotes.php | 2 +
app/Http/Controllers/Business/Pages.php | 1 +
app/Models/Business/CourierSurchargeQuote.php | 1 +
...lier_to_courier_surcharge_quotes_table.php | 32 ++++++++++++
.../courier-surcharge.blade.php | 11 ++--
.../Business/CourierSurchargePageTest.php | 50 ++++++++++++++++---
9 files changed, 111 insertions(+), 19 deletions(-)
create mode 100644 database/migrations/2026_09_22_010000_add_supplier_to_courier_surcharge_quotes_table.php
diff --git a/app/Business/CourierSurchargeAuditLogRepository.php b/app/Business/CourierSurchargeAuditLogRepository.php
index f6140b4b..ec24441e 100644
--- a/app/Business/CourierSurchargeAuditLogRepository.php
+++ b/app/Business/CourierSurchargeAuditLogRepository.php
@@ -20,6 +20,7 @@ class CourierSurchargeAuditLogRepository
'courier' => '快递名称',
'scope' => '快递归属',
'reconciler' => '对账人',
+ 'supplier' => '供应商',
'types' => '附加费类型',
'mode' => '结算方式',
'standard' => '计价标准',
diff --git a/app/Business/CourierSurchargeQuoteRepository.php b/app/Business/CourierSurchargeQuoteRepository.php
index 8f43052b..43064f2e 100644
--- a/app/Business/CourierSurchargeQuoteRepository.php
+++ b/app/Business/CourierSurchargeQuoteRepository.php
@@ -40,7 +40,11 @@ class CourierSurchargeQuoteRepository
public function ready(): bool
{
try {
- return Schema::hasTable((new CourierSurchargeQuote())->getTable()) && $this->types->ready();
+ $table = (new CourierSurchargeQuote())->getTable();
+
+ return Schema::hasTable($table)
+ && Schema::hasColumn($table, 'supplier')
+ && $this->types->ready();
} catch (QueryException) {
return false;
}
@@ -76,7 +80,7 @@ class CourierSurchargeQuoteRepository
}
$filtered = $all->filter(function (CourierSurchargeQuote $quote) use ($filters): bool {
- foreach (['courier', 'scope', 'reconciler', 'mode'] as $key) {
+ foreach (['courier', 'scope', 'reconciler', 'supplier', 'mode'] as $key) {
if (($filters[$key] ?? '') !== '' && (string) $quote->{$key} !== $filters[$key]) {
return false;
}
@@ -96,6 +100,7 @@ class CourierSurchargeQuoteRepository
$quote->courier,
$quote->scope,
$quote->reconciler,
+ (string) $quote->supplier,
$quote->typeLabel(),
$quote->mode,
$quote->standard,
@@ -139,6 +144,7 @@ class CourierSurchargeQuoteRepository
'modes' => collect(self::MODES),
'scopes' => self::SCOPES,
'reconcilers' => $all->pluck('reconciler')->filter()->unique()->sort()->values(),
+ 'suppliers' => $all->pluck('supplier')->filter()->unique()->sort()->values(),
'statuses' => ['已启用', '已停用'],
],
];
@@ -168,6 +174,9 @@ class CourierSurchargeQuoteRepository
->where('condition', trim((string) $data['condition']))
->whereDate('effective_at', (string) $data['effective_at']);
+ $supplier = trim((string) ($data['supplier'] ?? ''));
+ $supplier === '' ? $query->whereNull('supplier') : $query->where('supplier', $supplier);
+
if ($lockForUpdate) {
$query->lockForUpdate();
}
@@ -186,6 +195,7 @@ class CourierSurchargeQuoteRepository
trim((string) ($data['courier'] ?? '')),
trim((string) ($data['scope'] ?? '')),
trim((string) ($data['reconciler'] ?? '')),
+ trim((string) ($data['supplier'] ?? '')),
$this->normalizedTypes((array) ($data['types'] ?? [])),
trim((string) ($data['mode'] ?? '')),
trim((string) ($data['effective_at'] ?? '')),
@@ -279,6 +289,7 @@ class CourierSurchargeQuoteRepository
'courier' => $quote->courier,
'scope' => $quote->scope,
'reconciler' => $quote->reconciler,
+ 'supplier' => $quote->supplier,
'types' => array_values((array) $quote->types),
'mode' => $quote->mode,
'standard' => $quote->standard,
@@ -302,6 +313,7 @@ class CourierSurchargeQuoteRepository
'courier' => trim((string) $data['courier']),
'scope' => trim((string) $data['scope']),
'reconciler' => trim((string) $data['reconciler']),
+ 'supplier' => trim((string) ($data['supplier'] ?? '')) ?: null,
'types' => array_values(array_unique(array_map('strval', $data['types']))),
'mode' => trim((string) $data['mode']),
'standard' => trim((string) $data['standard']),
@@ -333,6 +345,7 @@ class CourierSurchargeQuoteRepository
'courier' => $quote->courier,
'scope' => $quote->scope,
'reconciler' => $quote->reconciler,
+ 'supplier' => $quote->supplier,
'types' => array_values((array) $quote->types),
'type' => $quote->typeLabel(),
'mode' => $quote->mode,
@@ -356,6 +369,7 @@ class CourierSurchargeQuoteRepository
'modes' => collect(self::MODES),
'scopes' => self::SCOPES,
'reconcilers' => collect(),
+ 'suppliers' => collect(),
'statuses' => ['已启用', '已停用'],
];
}
diff --git a/app/Business/CourierSurchargeQuoteWorkbookImporter.php b/app/Business/CourierSurchargeQuoteWorkbookImporter.php
index 22ecff3a..a630cc1f 100644
--- a/app/Business/CourierSurchargeQuoteWorkbookImporter.php
+++ b/app/Business/CourierSurchargeQuoteWorkbookImporter.php
@@ -29,6 +29,7 @@ class CourierSurchargeQuoteWorkbookImporter
'快递名称',
'快递归属',
'对账人',
+ '供应商',
'附加费类型',
'结算方式',
'计价标准',
@@ -64,7 +65,7 @@ class CourierSurchargeQuoteWorkbookImporter
$sheet->freezePane('A2');
$sheet->setAutoFilter($headerRange);
- $widths = [18, 14, 14, 24, 18, 30, 32, 14, 12, 30];
+ $widths = [18, 14, 14, 20, 24, 18, 30, 32, 14, 12, 30];
foreach ($widths as $index => $width) {
$column = Coordinate::stringFromColumnIndex($index + 1);
$sheet->getColumnDimension($column)->setWidth($width);
@@ -78,6 +79,7 @@ class CourierSurchargeQuoteWorkbookImporter
$instructions->fromArray([
['项目', '填写要求'],
['必填字段', '快递名称、快递归属、对账人、附加费类型、结算方式、计价标准、生效条件、生效日期、状态。'],
+ ['供应商', '选填;没有供应商时保持空白。'],
['附加费类型', '多种类型使用“、”分隔;只能填写当前已启用类型。当前可选:' . implode('、', $types)],
['快递归属', '自有快递 或 代发仓。'],
['结算方式', implode('、', CourierSurchargeQuoteRepository::MODES)],
@@ -92,8 +94,8 @@ class CourierSurchargeQuoteWorkbookImporter
]);
$instructions->getColumnDimension('A')->setWidth(18);
$instructions->getColumnDimension('B')->setWidth(90);
- $instructions->getStyle('A1:B9')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER)->setWrapText(true);
- foreach (range(1, 9) as $row) {
+ $instructions->getStyle('A1:B10')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER)->setWrapText(true);
+ foreach (range(1, 10) as $row) {
$instructions->getRowDimension($row)->setRowHeight($row === 1 ? 24 : 34);
}
@@ -144,7 +146,7 @@ class CourierSurchargeQuoteWorkbookImporter
}
if ($headers !== self::HEADERS) {
throw ValidationException::withMessages([
- 'import' => '表头不正确。请下载最新模板,并保持 10 个字段名称及顺序不变。',
+ 'import' => '表头不正确。请下载最新模板,并保持 11 个字段名称及顺序不变。',
]);
}
@@ -273,7 +275,7 @@ class CourierSurchargeQuoteWorkbookImporter
}
$errors = [];
- foreach (array_slice(self::HEADERS, 0, 9) as $header) {
+ foreach (['快递名称', '快递归属', '对账人', '附加费类型', '结算方式', '计价标准', '生效条件', '生效日期', '状态'] as $header) {
if ($display[$header] === '') {
$errors[] = $header . '不能为空。';
}
@@ -281,6 +283,7 @@ class CourierSurchargeQuoteWorkbookImporter
$this->checkLength($display, '快递名称', 191, $errors);
$this->checkLength($display, '对账人', 100, $errors);
+ $this->checkLength($display, '供应商', 191, $errors);
$this->checkLength($display, '计价标准', 2000, $errors);
$this->checkLength($display, '生效条件', 2000, $errors);
$this->checkLength($display, '备注', 2000, $errors);
@@ -333,6 +336,7 @@ class CourierSurchargeQuoteWorkbookImporter
'courier' => $display['快递名称'],
'scope' => $display['快递归属'],
'reconciler' => $display['对账人'],
+ 'supplier' => $display['供应商'] !== '' ? $display['供应商'] : null,
'types' => $types,
'mode' => $display['结算方式'],
'standard' => $display['计价标准'],
diff --git a/app/Http/Controllers/Business/CourierSurchargeQuotes.php b/app/Http/Controllers/Business/CourierSurchargeQuotes.php
index 5825c7f6..3364bf36 100644
--- a/app/Http/Controllers/Business/CourierSurchargeQuotes.php
+++ b/app/Http/Controllers/Business/CourierSurchargeQuotes.php
@@ -134,6 +134,7 @@ class CourierSurchargeQuotes extends Controller
'courier' => ['required', 'string', 'max:191'],
'scope' => ['required', 'in:' . implode(',', CourierSurchargeQuoteRepository::SCOPES)],
'reconciler' => ['required', 'string', 'max:100'],
+ 'supplier' => ['nullable', 'string', 'max:191'],
'types' => ['required', 'array', 'min:1', 'max:50'],
'types.*' => ['string', 'in:' . implode(',', $this->quotes->activeTypeNames())],
'mode' => ['required', 'in:' . implode(',', CourierSurchargeQuoteRepository::MODES)],
@@ -145,6 +146,7 @@ class CourierSurchargeQuotes extends Controller
$data['courier'] = trim($data['courier']);
$data['reconciler'] = trim($data['reconciler']);
+ $data['supplier'] = trim((string) ($data['supplier'] ?? '')) ?: null;
$data['types'] = array_values(array_unique($data['types']));
return $data;
diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php
index 1bc354b1..f62591be 100644
--- a/app/Http/Controllers/Business/Pages.php
+++ b/app/Http/Controllers/Business/Pages.php
@@ -1128,6 +1128,7 @@ class Pages extends Controller
'mode' => $clean(request('mode', ''), 40),
'scope' => $clean(request('scope', ''), 20),
'reconciler' => $clean(request('reconciler', ''), 40),
+ 'supplier' => $clean(request('supplier', ''), 191),
'status' => $clean(request('status', ''), 20),
];
$data = $this->courierSurchargeQuotes->pageData($filters);
diff --git a/app/Models/Business/CourierSurchargeQuote.php b/app/Models/Business/CourierSurchargeQuote.php
index 8cf53c08..5f3ff814 100644
--- a/app/Models/Business/CourierSurchargeQuote.php
+++ b/app/Models/Business/CourierSurchargeQuote.php
@@ -17,6 +17,7 @@ class CourierSurchargeQuote extends Model
'courier',
'scope',
'reconciler',
+ 'supplier',
'types',
'mode',
'standard',
diff --git a/database/migrations/2026_09_22_010000_add_supplier_to_courier_surcharge_quotes_table.php b/database/migrations/2026_09_22_010000_add_supplier_to_courier_surcharge_quotes_table.php
new file mode 100644
index 00000000..0ba54e23
--- /dev/null
+++ b/database/migrations/2026_09_22_010000_add_supplier_to_courier_surcharge_quotes_table.php
@@ -0,0 +1,32 @@
+string('supplier', 191)->nullable()->after('reconciler');
+ });
+ }
+
+ public function down(): void
+ {
+ if (! Schema::hasTable('business_courier_surcharge_quotes')
+ || ! Schema::hasColumn('business_courier_surcharge_quotes', 'supplier')) {
+ return;
+ }
+
+ Schema::table('business_courier_surcharge_quotes', function (Blueprint $table): void {
+ $table->dropColumn('supplier');
+ });
+ }
+};
diff --git a/resources/views/business/pages/courier-payables/courier-surcharge.blade.php b/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
index 5b72ec04..eafe0a63 100644
--- a/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
+++ b/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
@@ -17,6 +17,7 @@
['key' => 'courier', 'label' => '快递名称', 'exportable' => true],
['key' => 'scope', 'label' => '快递归属', 'exportable' => true],
['key' => 'reconciler', 'label' => '对账人', 'exportable' => true],
+ ['key' => 'supplier', 'label' => '供应商', 'exportable' => true],
['key' => 'type', 'label' => '附加费类型', 'exportable' => true],
['key' => 'mode', 'label' => '结算方式', 'exportable' => true],
['key' => 'standard', 'label' => '计价标准', 'exportable' => true],
@@ -114,10 +115,11 @@
tune筛选报价规则
维护建议:固定单价、按重量、按体积、按尺寸/最长边等方式直接维护标准;聚工单客服核实、取发出运费和按账单等方式只维护触发条件,具体金额由后续结算链路读取业务凭证。批量导入必须先预览校验,已有规则需明确选择跳过或更新。
@@ -242,6 +244,7 @@
+
@@ -482,7 +485,7 @@
});
root.querySelectorAll('[data-surcharge-edit]').forEach((button) => button.addEventListener('click', () => {
const row = button.closest('[data-surcharge-row]'); const data = JSON.parse(row.dataset.rule); form.action = row.dataset.updateUrl; methodField.value = 'PATCH'; title.textContent = '编辑报价规则';
- ['courier', 'scope', 'mode', 'standard', 'condition', 'effective_at', 'reconciler', 'remark'].forEach((key) => { const field = form.elements.namedItem(key); if (field) field.value = data[key] || ''; });
+ ['courier', 'scope', 'mode', 'standard', 'condition', 'effective_at', 'reconciler', 'supplier', 'remark'].forEach((key) => { const field = form.elements.namedItem(key); if (field) field.value = data[key] || ''; });
const selectedTypes = Array.isArray(data.types) ? data.types : (data.type || '').split('、').map((item) => item.trim()).filter(Boolean);
form.querySelectorAll('input[name="types[]"]').forEach((checkbox) => { checkbox.checked = selectedTypes.includes(checkbox.value); });
modal.classList.add('is-open');
diff --git a/tests/Feature/Business/CourierSurchargePageTest.php b/tests/Feature/Business/CourierSurchargePageTest.php
index ee6dd409..adf4f586 100644
--- a/tests/Feature/Business/CourierSurchargePageTest.php
+++ b/tests/Feature/Business/CourierSurchargePageTest.php
@@ -46,6 +46,8 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertSeeText('代发仓')
->assertSeeText('对账人')
->assertSeeText('张会计')
+ ->assertSeeText('供应商')
+ ->assertSeeText('曹县物流供应商')
->assertSeeText('附加费类型(可多选)')
->assertSeeText('列设置')
->assertSeeText('导出当前列')
@@ -61,7 +63,7 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertSee('data-surcharge-export-all', false)
->assertSee('data-column-reset', false)
->assertSee('name="types[]"', false)
- ->assertSeeInOrder(['data-column-key="courier"', 'data-column-key="scope"', 'data-column-key="reconciler"'], false)
+ ->assertSeeInOrder(['data-column-key="courier"', 'data-column-key="scope"', 'data-column-key="reconciler"', 'data-column-key="supplier"'], false)
->assertSeeText('曹县德邦')
->assertSeeText('超长费')
->assertSeeText('按尺寸/最长边')
@@ -151,6 +153,7 @@ class CourierSurchargePageTest extends FeatureTestCase
'courier' => '常州松宇货运',
'scope' => '代发仓',
'reconciler' => '李会计',
+ 'supplier' => '常州物流供应商',
'types' => ['送货费'],
'mode' => '按体积',
'standard' => '体积 ≤ 10 m³:50 元/单',
@@ -167,6 +170,7 @@ class CourierSurchargePageTest extends FeatureTestCase
'courier' => '常州松宇货运',
'scope' => '代发仓',
'reconciler' => '李会计',
+ 'supplier' => '常州物流供应商',
'status' => '已启用',
]))
->assertOk()
@@ -174,6 +178,7 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertSeeText('送货费')
->assertSeeText('代发仓')
->assertSeeText('李会计')
+ ->assertSeeText('常州物流供应商')
->assertSeeText('当前规则')
->assertDontSee('曹县德邦 | ', false)
->assertDontSee('霸州百世快运-雅柏 | ', false);
@@ -188,6 +193,7 @@ class CourierSurchargePageTest extends FeatureTestCase
'courier' => '测试快递',
'scope' => '代发仓',
'reconciler' => '李会计',
+ 'supplier' => '',
'types' => ['拦截费', '改地址'],
'mode' => '固定单价',
'standard' => '12 元/单',
@@ -203,6 +209,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$quote = CourierSurchargeQuote::query()->where('courier', '测试快递')->firstOrFail();
$this->assertSame(['拦截费', '改地址'], $quote->types);
+ $this->assertNull($quote->supplier);
$this->assertSame(CourierSurchargeQuote::STATUS_ACTIVE, $quote->status);
$this->assertSame((int) user_id(), (int) $quote->created_by);
$this->assertSame((int) user_id(), (int) $quote->updated_by);
@@ -210,11 +217,13 @@ class CourierSurchargePageTest extends FeatureTestCase
$this->patch(route('business.courier-surcharge.update', ['quote' => $quote->id]), [
...$payload,
'courier' => '测试快递(已修改)',
+ 'supplier' => '测试供应商',
'types' => ['保费'],
])->assertRedirect();
$quote->refresh();
$this->assertSame('测试快递(已修改)', $quote->courier);
+ $this->assertSame('测试供应商', $quote->supplier);
$this->assertSame(['保费'], $quote->types);
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
@@ -233,6 +242,7 @@ class CourierSurchargePageTest extends FeatureTestCase
'courier' => '日志测试快递',
'scope' => '自有快递',
'reconciler' => '周会计',
+ 'supplier' => '原供应商',
'types' => ['进仓费'],
'mode' => '固定单价',
'standard' => '10 元/单',
@@ -255,6 +265,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$this->patch(route('business.courier-surcharge.update', ['quote' => $quote->id]), [
...$payload,
+ 'supplier' => '新供应商',
'standard' => '12 元/单',
'remark' => '调整后标准',
])->assertRedirect();
@@ -268,6 +279,8 @@ class CourierSurchargePageTest extends FeatureTestCase
$this->assertSame('12 元/单', data_get($updated->changes, 'after.standard'));
$this->assertSame('初始标准', data_get($updated->changes, 'before.remark'));
$this->assertSame('调整后标准', data_get($updated->changes, 'after.remark'));
+ $this->assertSame('原供应商', data_get($updated->changes, 'before.supplier'));
+ $this->assertSame('新供应商', data_get($updated->changes, 'after.supplier'));
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
'status' => CourierSurchargeQuote::STATUS_INACTIVE,
@@ -297,6 +310,7 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertSeeText('停用')
->assertSeeText('启用')
->assertSeeText($this->user->name)
+ ->assertSeeText('供应商:原供应商 → 新供应商')
->assertSeeText('计价标准:10 元/单 → 12 元/单');
}
@@ -383,10 +397,10 @@ class CourierSurchargePageTest extends FeatureTestCase
try {
$this->assertSame(
CourierSurchargeQuoteWorkbookImporter::HEADERS,
- $spreadsheet->getSheet(0)->rangeToArray('A1:J1')[0],
+ $spreadsheet->getSheet(0)->rangeToArray('A1:K1')[0],
);
$this->assertSame('填写说明', $spreadsheet->getSheet(1)->getTitle());
- $this->assertStringContainsString('不会静默覆盖', (string) $spreadsheet->getSheet(1)->getCell('B8')->getValue());
+ $this->assertStringContainsString('不会静默覆盖', (string) $spreadsheet->getSheet(1)->getCell('B9')->getValue());
} finally {
$spreadsheet->disconnectWorksheets();
}
@@ -397,7 +411,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$file = $this->workbook([[
- '导入测试快递', '代发仓', '李会计', '进仓费、超长费', '固定单价',
+ '导入测试快递', '代发仓', '李会计', '导入供应商', '进仓费、超长费', '固定单价',
'12.50 元/单', '入仓即收取', '2026-09-22', '已停用', '批量导入测试',
]]);
@@ -416,6 +430,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$quote = CourierSurchargeQuote::query()->where('courier', '导入测试快递')->firstOrFail();
$this->assertSame(['进仓费', '超长费'], $quote->types);
+ $this->assertSame('导入供应商', $quote->supplier);
$this->assertSame(CourierSurchargeQuote::STATUS_INACTIVE, $quote->status);
$this->assertSame('批量导入测试', $quote->remark);
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
@@ -441,7 +456,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$previewResponse = $this->post(route('business.courier-surcharge.import.preview'), [
'import_file' => $this->workbook([[
- '', '未知归属', '李会计', '超长费、虚构费', '未知方式', '',
+ '', '未知归属', '李会计', '', '超长费、虚构费', '未知方式', '',
'测试条件', '2026-02-31', '未知状态', '',
]]),
]);
@@ -476,7 +491,7 @@ class CourierSurchargePageTest extends FeatureTestCase
])->assertSessionHasErrors('import');
$rows = array_fill(0, CourierSurchargeQuoteWorkbookImporter::MAX_ROWS + 1, [
- '测试快递', '自有快递', '周会计', '进仓费', '固定单价',
+ '测试快递', '自有快递', '周会计', '', '进仓费', '固定单价',
'1 元/单', '测试条件', '2026-09-22', '已启用', '',
]);
$this->withExceptionHandling()->post(route('business.courier-surcharge.import.preview'), [
@@ -489,7 +504,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$duplicateRow = [
- '文件内重复快递', '自有快递', '周会计', '进仓费', '固定单价',
+ '文件内重复快递', '自有快递', '周会计', '重复测试供应商', '进仓费', '固定单价',
'5 元/单', '重复条件', '2026-09-22', '已启用', '',
];
$response = $this->post(route('business.courier-surcharge.import.preview'), [
@@ -503,7 +518,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$existing = $this->createQuote();
$existingRow = [
- $existing->courier, $existing->scope, $existing->reconciler, $existing->typeLabel(), $existing->mode,
+ $existing->courier, $existing->scope, $existing->reconciler, $existing->supplier, $existing->typeLabel(), $existing->mode,
'新计价标准', $existing->condition, $existing->effective_at->format('Y-m-d'), '已停用', '更新后的备注',
];
$skipPreview = $this->post(route('business.courier-surcharge.import.preview'), [
@@ -534,6 +549,24 @@ class CourierSurchargePageTest extends FeatureTestCase
]);
}
+ public function test_import_duplicate_identity_includes_optional_supplier(): void
+ {
+ $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
+ $this->loginAs();
+ $base = [
+ '供应商区分快递', '自有快递', '周会计', '供应商甲', '进仓费', '固定单价',
+ '5 元/单', '相同条件', '2026-09-22', '已启用', '',
+ ];
+ $otherSupplier = $base;
+ $otherSupplier[3] = '供应商乙';
+
+ $this->post(route('business.courier-surcharge.import.preview'), [
+ 'import_file' => $this->workbook([$base, $otherSupplier]),
+ ])->assertSessionHas('courierSurchargeImportPreview', function (array $preview): bool {
+ return $preview['summary'] === ['total' => 2, 'valid' => 2, 'new' => 2, 'duplicate' => 0, 'invalid' => 0];
+ });
+ }
+
public function test_import_commit_rejects_expired_or_other_context_preview(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
@@ -600,6 +633,7 @@ class CourierSurchargePageTest extends FeatureTestCase
'courier' => '曹县德邦',
'scope' => '自有快递',
'reconciler' => '张会计',
+ 'supplier' => '曹县物流供应商',
'types' => ['超长费'],
'mode' => '按尺寸/最长边',
'standard' => '最长边 > 160 cm:8 元/单',
--
2.47.2
From d11693dc97e43dee951312af7107ba0ba53c9a26 Mon Sep 17 00:00:00 2001
From: zhoushengchen <3057083327@qq.com>
Date: Tue, 22 Sep 2026 18:32:43 +0800
Subject: [PATCH 2/4] =?UTF-8?q?=E6=8C=89=E6=8A=A5=E4=BB=B7=E7=AD=9B?=
=?UTF-8?q?=E9=80=89=E9=99=84=E5=8A=A0=E8=B4=B9=E6=93=8D=E4=BD=9C=E6=97=A5?=
=?UTF-8?q?=E5=BF=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CourierSurchargeAuditLogRepository.php | 2 +
.../courier-surcharge.blade.php | 58 ++++++++++++++++---
.../Business/CourierSurchargePageTest.php | 14 ++++-
3 files changed, 65 insertions(+), 9 deletions(-)
diff --git a/app/Business/CourierSurchargeAuditLogRepository.php b/app/Business/CourierSurchargeAuditLogRepository.php
index ec24441e..eb3acdd5 100644
--- a/app/Business/CourierSurchargeAuditLogRepository.php
+++ b/app/Business/CourierSurchargeAuditLogRepository.php
@@ -99,6 +99,8 @@ class CourierSurchargeAuditLogRepository
'actor_name' => $log->actor?->name ?: '系统',
'action' => $log->action,
'action_label' => self::ACTION_LABELS[$log->action] ?? $log->action,
+ 'target_type' => $log->target_type,
+ 'target_id' => $log->target_id,
'target_type_label' => self::TARGET_LABELS[$log->target_type] ?? $log->target_type,
'target_label' => $log->target_label,
'changes_label' => $this->changesLabel($log),
diff --git a/resources/views/business/pages/courier-payables/courier-surcharge.blade.php b/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
index eafe0a63..7996f7e8 100644
--- a/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
+++ b/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
@@ -63,6 +63,10 @@
.sm-audit-table td:nth-child(5) { min-width:24rem; white-space:normal; line-height:1.5; }
.sm-audit-action { display:inline-flex; align-items:center; padding:.15rem .45rem; border-radius:999px; background:var(--sm-soft); color:var(--sm-accent); font-size:.7rem; font-weight:700; }
.sm-audit-empty { padding:1.8rem 1rem; text-align:center; color:var(--sm-muted); }
+ .sm-audit-empty[hidden],.sm-audit-table-wrap[hidden],.sm-audit-table tr[hidden] { display:none; }
+ .sm-table tr[data-surcharge-row] { cursor:pointer; }
+ .sm-table tr[data-surcharge-row]:focus-visible { outline:2px solid var(--sm-accent); outline-offset:-2px; }
+ .sm-table tr.is-log-selected td,.sm-table tr.is-log-selected:hover td,.sm-table tr.is-log-selected [data-column-key="id"],.sm-table tr.is-log-selected [data-column-key="actions"] { background:var(--sm-soft); }
.sm-column-picker { position:relative; } .sm-column-picker__panel { position:absolute; right:0; z-index:20; display:grid; width:min(22rem,calc(100vw - 2rem)); gap:.65rem; margin-top:.35rem; padding:.8rem; border:1px solid var(--sm-border); border-radius:.55rem; background:#fff; box-shadow:0 4px 8px rgba(23,23,23,.1); } .sm-column-picker__panel[hidden] { display:none; } .sm-column-picker__intro { display:grid; gap:.2rem; color:var(--sm-muted); font-size:.7rem; line-height:1.45; } .sm-column-picker__intro strong { color:var(--sm-ink); font-size:.78rem; } .sm-visually-hidden { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; } .sm-column-picker__list { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.35rem; max-height:15rem; overflow:auto; } .sm-column-picker__option { display:flex; align-items:center; gap:.35rem; min-width:0; padding:.35rem .4rem; border:1px solid transparent; border-radius:.35rem; color:var(--sm-ink); font-size:.7rem; cursor:grab; } .sm-column-picker__option:hover,.sm-column-picker__option.is-drop-target { border-color:var(--sm-border); background:#fffdf8; } .sm-column-picker__option.is-fixed { cursor:default; } .sm-column-picker__option.is-dragging { opacity:.55; } .sm-column-picker__option input { flex:none; accent-color:var(--sm-accent); } .sm-column-picker__option .material-icons-outlined { color:var(--sm-muted); font-size:.95rem; } .sm-column-picker__foot { display:flex; justify-content:flex-end; padding-top:.35rem; border-top:1px solid var(--sm-border); }
.sm-badge { display:inline-flex; align-items:center; padding:.15rem .5rem; border-radius:999px; font-size:.7rem; font-weight:700; } .sm-badge.is-active { background:#edf5e8; color:#53793e; } .sm-badge.is-inactive { background:#fff4df; color:#935f07; }
.sm-empty { padding:3rem 1rem; text-align:center; color:var(--sm-muted); } .sm-rule-note { padding:1rem 1.15rem; color:var(--sm-muted); font-size:.75rem; line-height:1.65; background:#fffdf8; }
@@ -145,7 +149,7 @@
@endforeach
@forelse ($rows as $row)
-
+
| {{ $row['id'] }} | {{ $row['courier'] }} | {{ $row['scope'] }} | {{ $row['reconciler'] }} | {{ $row['supplier'] ?: '—' }} | {{ $row['type'] }} | {{ $row['mode'] }} | {{ $row['standard'] }} | {{ $row['condition'] }} | {{ $row['effective_at'] }} | {{ $row['status'] }} | {{ $row['remark'] ?: '—' }} | |
@empty
@@ -155,19 +159,19 @@
维护建议:固定单价、按重量、按体积、按尺寸/最长边等方式直接维护标准;聚工单客服核实、取发出运费和按账单等方式只维护触发条件,具体金额由后续结算链路读取业务凭证。批量导入必须先预览校验,已有规则需明确选择跳过或更新。
-
-
history操作日志
记录当前公司最近 50 条新增、更改、启用和停用操作
+
+
@if (!$auditReady)
操作日志表尚未就绪,完成迁移后将从首次新操作开始记录。
- @elseif ($auditLogs->isEmpty())
-
暂无操作日志。新增、编辑或切换报价规则/附加费类型状态后,记录会显示在这里。
@else
-
+
请选择上方一条报价
+
该报价暂无操作日志。
+
| 操作时间 | 操作人 | 动作 | 对象 | 变更内容 |
@foreach ($auditLogs as $log)
-
+
| {{ $log['occurred_at'] }} |
{{ $log['actor_name'] }} |
{{ $log['action_label'] }} |
@@ -281,6 +285,13 @@
if (!root) return;
const table = root.querySelector('[data-surcharge-table]');
const tableWrap = root.querySelector('[data-surcharge-table-wrap]');
+ const auditPanel = root.querySelector('[data-surcharge-audit-panel]');
+ const auditTitle = root.querySelector('[data-surcharge-audit-title]');
+ const auditHint = root.querySelector('[data-surcharge-audit-hint]');
+ const auditPlaceholder = root.querySelector('[data-surcharge-audit-placeholder]');
+ const auditEmpty = root.querySelector('[data-surcharge-audit-empty]');
+ const auditTableWrap = root.querySelector('[data-surcharge-audit-table-wrap]');
+ const auditRows = [...root.querySelectorAll('[data-audit-log]')];
const picker = root.querySelector('[data-column-picker]');
const columnToggle = picker?.querySelector('[data-column-toggle]');
const columnPanel = picker?.querySelector('[data-column-options]') || picker?.querySelector('.sm-column-picker__panel');
@@ -456,6 +467,39 @@
tableWrap.classList.toggle('is-scrolled-x', tableWrap.scrollLeft > 0);
}, { passive: true });
+ const selectQuoteLog = (row) => {
+ const quoteId = String(row.dataset.auditQuoteId || '');
+ const quoteLabel = row.dataset.auditQuoteLabel || '当前报价';
+ root.querySelectorAll('[data-surcharge-row]').forEach((candidate) => {
+ const selected = candidate === row;
+ candidate.classList.toggle('is-log-selected', selected);
+ candidate.setAttribute('aria-selected', String(selected));
+ });
+ let visibleCount = 0;
+ auditRows.forEach((auditRow) => {
+ const visible = auditRow.dataset.targetType === 'quote' && auditRow.dataset.targetId === quoteId;
+ auditRow.hidden = !visible;
+ if (visible) visibleCount++;
+ });
+ if (auditTitle) auditTitle.textContent = `操作日志 · ${quoteLabel}`;
+ if (auditHint) auditHint.textContent = visibleCount ? `${visibleCount} 条记录` : '暂无记录';
+ if (auditPlaceholder) auditPlaceholder.hidden = true;
+ if (auditEmpty) auditEmpty.hidden = visibleCount > 0;
+ if (auditTableWrap) auditTableWrap.hidden = visibleCount === 0;
+ auditPanel?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+ };
+ root.querySelectorAll('[data-surcharge-row]').forEach((row) => {
+ row.addEventListener('click', (event) => {
+ if (event.target.closest('button,a,input,select,textarea,form,label')) return;
+ selectQuoteLog(row);
+ });
+ row.addEventListener('keydown', (event) => {
+ if (!['Enter', ' '].includes(event.key)) return;
+ event.preventDefault();
+ selectQuoteLog(row);
+ });
+ });
+
const modal = root.querySelector('[data-surcharge-modal]');
const typeModal = root.querySelector('[data-type-modal]');
const importModal = root.querySelector('[data-surcharge-import-modal]');
diff --git a/tests/Feature/Business/CourierSurchargePageTest.php b/tests/Feature/Business/CourierSurchargePageTest.php
index adf4f586..c82d73d1 100644
--- a/tests/Feature/Business/CourierSurchargePageTest.php
+++ b/tests/Feature/Business/CourierSurchargePageTest.php
@@ -58,12 +58,16 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertSeeText('规则编号与操作列固定显示')
->assertSee('data-column-storage-key="caiwu:table-columns:', false)
->assertSee('data-surcharge-table', false)
+ ->assertSee('data-surcharge-audit-panel', false)
+ ->assertSee('data-surcharge-audit-placeholder', false)
+ ->assertSee('data-surcharge-audit-table-wrap hidden', false)
->assertSee('data-column-check', false)
->assertSee('data-surcharge-export-current', false)
->assertSee('data-surcharge-export-all', false)
->assertSee('data-column-reset', false)
->assertSee('name="types[]"', false)
->assertSeeInOrder(['data-column-key="courier"', 'data-column-key="scope"', 'data-column-key="reconciler"', 'data-column-key="supplier"'], false)
+ ->assertSee('data-audit-quote-id="', false)
->assertSeeText('曹县德邦')
->assertSeeText('超长费')
->assertSeeText('按尺寸/最长边')
@@ -302,9 +306,10 @@ class CourierSurchargePageTest extends FeatureTestCase
'action' => CourierSurchargeAuditLog::ACTION_ACTIVATED,
]);
- $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
+ $response = $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeText('操作日志')
+ ->assertSeeText('请选择上方一条报价')
->assertSeeText('新增')
->assertSeeText('更改')
->assertSeeText('停用')
@@ -312,6 +317,11 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertSeeText($this->user->name)
->assertSeeText('供应商:原供应商 → 新供应商')
->assertSeeText('计价标准:10 元/单 → 12 元/单');
+
+ $response
+ ->assertSee('data-audit-quote-id="' . $quote->id . '"', false)
+ ->assertSee('data-audit-log data-target-type="quote" data-target-id="' . $quote->id . '" hidden', false)
+ ->assertSee('aria-selected="false"', false);
}
public function test_type_operations_are_recorded_and_other_company_logs_are_hidden(): void
@@ -353,7 +363,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
- ->assertSeeText('附加费类型 · 日志专用类型')
+ ->assertSee('data-audit-log data-target-type="type" data-target-id="' . $type->id . '" hidden', false)
->assertDontSeeText('其他公司的私有类型');
}
--
2.47.2
From 98de7ae6c619ed3201e136f52332ce2d94e677f7 Mon Sep 17 00:00:00 2001
From: zhoushengchen <3057083327@qq.com>
Date: Tue, 22 Sep 2026 18:36:40 +0800
Subject: [PATCH 3/4] =?UTF-8?q?=E6=8C=89=E5=BD=95=E5=85=A5=E9=A1=BA?=
=?UTF-8?q?=E5=BA=8F=E6=8E=92=E5=88=97=E9=99=84=E5=8A=A0=E8=B4=B9=E6=8A=A5?=
=?UTF-8?q?=E4=BB=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CourierSurchargeQuoteRepository.php | 3 +-
.../Business/CourierSurchargePageTest.php | 37 +++++++++++++++++++
2 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/app/Business/CourierSurchargeQuoteRepository.php b/app/Business/CourierSurchargeQuoteRepository.php
index 43064f2e..bd084c20 100644
--- a/app/Business/CourierSurchargeQuoteRepository.php
+++ b/app/Business/CourierSurchargeQuoteRepository.php
@@ -67,8 +67,7 @@ class CourierSurchargeQuoteRepository
try {
$all = CourierSurchargeQuote::query()
- ->orderByDesc('effective_at')
- ->orderByDesc('id')
+ ->orderBy('id')
->get();
} catch (QueryException) {
return [
diff --git a/tests/Feature/Business/CourierSurchargePageTest.php b/tests/Feature/Business/CourierSurchargePageTest.php
index c82d73d1..727675d5 100644
--- a/tests/Feature/Business/CourierSurchargePageTest.php
+++ b/tests/Feature/Business/CourierSurchargePageTest.php
@@ -188,6 +188,43 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertDontSee('霸州百世快运-雅柏 | ', false);
}
+ public function test_courier_surcharge_page_keeps_creation_order_instead_of_effective_date_order(): void
+ {
+ $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
+ $first = CourierSurchargeQuote::query()->create([
+ 'company_id' => company_id(),
+ 'courier' => '顺序测试第一条',
+ 'scope' => '自有快递',
+ 'reconciler' => '苏木',
+ 'supplier' => null,
+ 'types' => ['送货费'],
+ 'mode' => '固定单价',
+ 'standard' => '1 元/单',
+ 'condition' => '第一条',
+ 'effective_at' => '2027-12-31',
+ 'status' => CourierSurchargeQuote::STATUS_ACTIVE,
+ ]);
+ $second = CourierSurchargeQuote::query()->create([
+ 'company_id' => company_id(),
+ 'courier' => '顺序测试第二条',
+ 'scope' => '自有快递',
+ 'reconciler' => '苏木',
+ 'supplier' => null,
+ 'types' => ['进仓费'],
+ 'mode' => '固定单价',
+ 'standard' => '2 元/单',
+ 'condition' => '第二条',
+ 'effective_at' => '2026-01-01',
+ 'status' => CourierSurchargeQuote::STATUS_ACTIVE,
+ ]);
+
+ $this->assertLessThan($second->id, $first->id);
+ $this->loginAs()
+ ->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
+ ->assertOk()
+ ->assertSeeInOrder(['顺序测试第一条', '顺序测试第二条']);
+ }
+
public function test_courier_surcharge_store_update_and_status_are_persistent(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
--
2.47.2
From c205742c4852de5fd287a9fdeee63ab7d3697649 Mon Sep 17 00:00:00 2001
From: zhoushengchen <3057083327@qq.com>
Date: Tue, 22 Sep 2026 18:45:02 +0800
Subject: [PATCH 4/4] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=B0=83=E6=95=B4?=
=?UTF-8?q?=E9=99=84=E5=8A=A0=E8=B4=B9=E6=8A=A5=E4=BB=B7=E5=88=97=E5=AE=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../courier-surcharge.blade.php | 87 +++++++++++++++++--
.../Business/CourierSurchargePageTest.php | 7 +-
2 files changed, 87 insertions(+), 7 deletions(-)
diff --git a/resources/views/business/pages/courier-payables/courier-surcharge.blade.php b/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
index 7996f7e8..bfc82ce6 100644
--- a/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
+++ b/resources/views/business/pages/courier-payables/courier-surcharge.blade.php
@@ -67,6 +67,12 @@
.sm-table tr[data-surcharge-row] { cursor:pointer; }
.sm-table tr[data-surcharge-row]:focus-visible { outline:2px solid var(--sm-accent); outline-offset:-2px; }
.sm-table tr.is-log-selected td,.sm-table tr.is-log-selected:hover td,.sm-table tr.is-log-selected [data-column-key="id"],.sm-table tr.is-log-selected [data-column-key="actions"] { background:var(--sm-soft); }
+ .sm-table td { overflow:hidden; text-overflow:ellipsis; }
+ .sm-table th .sm-table__resize { position:absolute; top:0; right:-.3rem; z-index:7; width:.6rem; height:100%; cursor:col-resize; touch-action:none; }
+ .sm-table th .sm-table__resize::after { position:absolute; top:20%; bottom:20%; left:calc(50% - .5px); width:1px; background:var(--sm-border); content:""; }
+ .sm-table th .sm-table__resize:hover::after,.sm-table th .sm-table__resize:focus-visible::after { width:2px; background:var(--sm-accent); }
+ .sm-table th .sm-table__resize:focus-visible { outline:2px solid var(--sm-accent); outline-offset:-2px; }
+ .sm-table.is-resizing,.sm-table.is-resizing * { cursor:col-resize !important; user-select:none; }
.sm-column-picker { position:relative; } .sm-column-picker__panel { position:absolute; right:0; z-index:20; display:grid; width:min(22rem,calc(100vw - 2rem)); gap:.65rem; margin-top:.35rem; padding:.8rem; border:1px solid var(--sm-border); border-radius:.55rem; background:#fff; box-shadow:0 4px 8px rgba(23,23,23,.1); } .sm-column-picker__panel[hidden] { display:none; } .sm-column-picker__intro { display:grid; gap:.2rem; color:var(--sm-muted); font-size:.7rem; line-height:1.45; } .sm-column-picker__intro strong { color:var(--sm-ink); font-size:.78rem; } .sm-visually-hidden { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; } .sm-column-picker__list { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.35rem; max-height:15rem; overflow:auto; } .sm-column-picker__option { display:flex; align-items:center; gap:.35rem; min-width:0; padding:.35rem .4rem; border:1px solid transparent; border-radius:.35rem; color:var(--sm-ink); font-size:.7rem; cursor:grab; } .sm-column-picker__option:hover,.sm-column-picker__option.is-drop-target { border-color:var(--sm-border); background:#fffdf8; } .sm-column-picker__option.is-fixed { cursor:default; } .sm-column-picker__option.is-dragging { opacity:.55; } .sm-column-picker__option input { flex:none; accent-color:var(--sm-accent); } .sm-column-picker__option .material-icons-outlined { color:var(--sm-muted); font-size:.95rem; } .sm-column-picker__foot { display:flex; justify-content:flex-end; padding-top:.35rem; border-top:1px solid var(--sm-border); }
.sm-badge { display:inline-flex; align-items:center; padding:.15rem .5rem; border-radius:999px; font-size:.7rem; font-weight:700; } .sm-badge.is-active { background:#edf5e8; color:#53793e; } .sm-badge.is-inactive { background:#fff4df; color:#935f07; }
.sm-empty { padding:3rem 1rem; text-align:center; color:var(--sm-muted); } .sm-rule-note { padding:1rem 1.15rem; color:var(--sm-muted); font-size:.75rem; line-height:1.65; background:#fffdf8; }
@@ -145,7 +151,7 @@
当前筛选结果(全部):{{ $rows->count() }} 条 · 共 {{ $rows->isEmpty() ? 0 : 1 }} 页规则编号与操作列固定显示;列设置仅保存于当前用户
@foreach ($columns as $column)
- |
+ |
@endforeach
@forelse ($rows as $row)
@@ -303,7 +309,10 @@
const mutableKeys = defaultOrder.filter((key) => !fixedKeys.has(key));
const storageKey = root.dataset.columnStorageKey || 'caiwu:table-columns:courier-surcharge:quotes';
const emptyCell = table?.querySelector('[data-empty-cell]');
- let state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
+ const minColumnWidth = 72;
+ const maxColumnWidth = 720;
+ const clampColumnWidth = (width) => Math.min(maxColumnWidth, Math.max(minColumnWidth, Math.round(Number(width) || minColumnWidth)));
+ let state = { order: [...defaultOrder], visible: new Set(defaultOrder), widths: {} };
const readState = () => {
try {
@@ -315,16 +324,19 @@
const visible = Array.isArray(saved.visible)
? new Set(saved.visible.filter((key) => defaultOrder.includes(key)))
: new Set(defaultOrder);
+ const widths = Object.fromEntries(Object.entries(saved.widths || {})
+ .filter(([key, width]) => defaultOrder.includes(key) && Number.isFinite(Number(width)))
+ .map(([key, width]) => [key, clampColumnWidth(width)]));
fixedKeys.forEach((key) => visible.add(key));
- state = { order, visible };
+ state = { order, visible, widths };
} catch (_) {
- state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
+ state = { order: [...defaultOrder], visible: new Set(defaultOrder), widths: {} };
}
};
const writeState = () => {
try {
- window.localStorage.setItem(storageKey, JSON.stringify({ order: state.order, visible: [...state.visible] }));
+ window.localStorage.setItem(storageKey, JSON.stringify({ order: state.order, visible: [...state.visible], widths: state.widths }));
} catch (_) {}
};
@@ -345,6 +357,22 @@
keys.forEach((key) => { if (cells.has(key)) row.appendChild(cells.get(key)); });
};
+ const applyColumnWidths = () => {
+ if (!table) return;
+ table.querySelectorAll('[data-column-key]').forEach((cell) => {
+ const width = state.widths[cell.dataset.columnKey];
+ if (Number.isFinite(width)) {
+ cell.style.width = `${width}px`;
+ cell.style.minWidth = `${width}px`;
+ cell.style.maxWidth = `${width}px`;
+ } else {
+ cell.style.removeProperty('width');
+ cell.style.removeProperty('min-width');
+ cell.style.removeProperty('max-width');
+ }
+ });
+ };
+
const applyColumns = (save = true) => {
if (!table) return;
const keys = orderedKeys();
@@ -362,6 +390,7 @@
const options = new Map([...columnList.querySelectorAll('[data-column-option]')].map((option) => [option.dataset.columnKey, option]));
keys.forEach((key) => { if (options.has(key)) columnList.appendChild(options.get(key)); });
}
+ applyColumnWidths();
if (save) writeState();
};
@@ -416,12 +445,13 @@
applyColumns();
}));
picker?.querySelector('[data-column-reset]')?.addEventListener('click', () => {
- state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
+ state = { order: [...defaultOrder], visible: new Set(defaultOrder), widths: {} };
applyColumns();
});
const dragSource = (element, sourceKey) => {
element.addEventListener('dragstart', (event) => {
+ if (event.target.closest?.('[data-column-resize]')) { event.preventDefault(); return; }
if (fixedKeys.has(sourceKey)) { event.preventDefault(); return; }
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', sourceKey);
@@ -445,6 +475,51 @@
moveMutableKey(event.dataTransfer.getData('text/plain'), key, event.offsetX < header.offsetWidth / 2);
});
});
+ headers.forEach((header) => {
+ const handle = header.querySelector('[data-column-resize]');
+ const key = header.dataset.columnKey;
+ if (!handle || !key) return;
+ const setWidth = (width, save = false) => {
+ state.widths[key] = clampColumnWidth(width);
+ applyColumnWidths();
+ if (save) writeState();
+ };
+ handle.addEventListener('pointerdown', (event) => {
+ if (event.button !== 0) return;
+ event.preventDefault();
+ event.stopPropagation();
+ const startX = event.clientX;
+ const startWidth = header.getBoundingClientRect().width;
+ table?.classList.add('is-resizing');
+ handle.setPointerCapture?.(event.pointerId);
+ const move = (moveEvent) => setWidth(startWidth + moveEvent.clientX - startX);
+ const finish = (finishEvent) => {
+ handle.removeEventListener('pointermove', move);
+ handle.removeEventListener('pointerup', finish);
+ handle.removeEventListener('pointercancel', finish);
+ if (handle.hasPointerCapture?.(finishEvent.pointerId)) handle.releasePointerCapture(finishEvent.pointerId);
+ table?.classList.remove('is-resizing');
+ writeState();
+ };
+ handle.addEventListener('pointermove', move);
+ handle.addEventListener('pointerup', finish);
+ handle.addEventListener('pointercancel', finish);
+ });
+ handle.addEventListener('keydown', (event) => {
+ if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
+ event.preventDefault();
+ event.stopPropagation();
+ const current = state.widths[key] || header.getBoundingClientRect().width;
+ setWidth(current + (event.key === 'ArrowRight' ? 10 : -10), true);
+ });
+ handle.addEventListener('dblclick', (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ delete state.widths[key];
+ applyColumnWidths();
+ writeState();
+ });
+ });
columnList?.querySelectorAll('[data-column-option]').forEach((option) => {
if (option.dataset.columnKey === undefined || fixedKeys.has(option.dataset.columnKey)) return;
const key = option.dataset.columnKey;
diff --git a/tests/Feature/Business/CourierSurchargePageTest.php b/tests/Feature/Business/CourierSurchargePageTest.php
index 727675d5..133c5c77 100644
--- a/tests/Feature/Business/CourierSurchargePageTest.php
+++ b/tests/Feature/Business/CourierSurchargePageTest.php
@@ -25,7 +25,7 @@ class CourierSurchargePageTest extends FeatureTestCase
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->createQuote();
- $this->loginAs()
+ $response = $this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'courier-surcharge',
@@ -62,6 +62,8 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertSee('data-surcharge-audit-placeholder', false)
->assertSee('data-surcharge-audit-table-wrap hidden', false)
->assertSee('data-column-check', false)
+ ->assertSee('data-column-resize', false)
+ ->assertSee('aria-orientation="vertical"', false)
->assertSee('data-surcharge-export-current', false)
->assertSee('data-surcharge-export-all', false)
->assertSee('data-column-reset', false)
@@ -74,6 +76,9 @@ class CourierSurchargePageTest extends FeatureTestCase
->assertSeeText('8 元/单')
->assertSeeText('已启用')
->assertSeeText('后续结算页面将按启用中的规则计算');
+
+ $this->assertSame(13, substr_count($response->getContent(), 'data-column-resize>'));
+ $this->assertStringContainsString('widths: state.widths', $response->getContent());
}
public function test_courier_surcharge_type_can_be_created_and_duplicate_is_rejected(): void
--
2.47.2