feat(logistics-arrival): 重置 / 恢复默认 / 清空排除 三个按钮语义拆开

过去三个按钮都只是"改输入、等用户点查询",而且重置只清日期+关键词、另两个只改排除词,
最终看起来结果一样。现在各自一件事,点下去立即查询:

- 重置:签收日期、关键词、排除关键词全部清空 → 没有任何筛选条件
  (空日期仍由服务端兜底成本月 1 号~昨日;新增 dateRangeCleared 标记,
   让输入框保持空白而不是被兜底值回填)
- 恢复默认:签收日期回到本月 1 号~昨日(显式填回输入框)+ 排除关键词恢复系统默认配置
- 清空排除:签收日期回到本月 1 号~昨日 + 排除关键词置空

配套:`logisticsArrivalSigningDurationDefaultRange()` 从过滤器规范化里抽出来共用(单一来源)、
按钮 hover 说明 + 筛选栏底部图例、去掉旧的"填好后点查询"提示、补视图层测试。
This commit is contained in:
codex 2026-10-07 11:05:35 +08:00 committed by WuYongJie
parent 787fcb67bb
commit 1b49125ffa
5 changed files with 142 additions and 22 deletions

View File

@ -149,7 +149,15 @@ sudo bash scripts/install-logistics-arrival-signing-duration-snapshot-timer.sh
- 解析规则由 `splitSearchKeywords()` 统一提供,服务端缓存键按关键词集合(排序后)计算,输入顺序不同不会重复缓存。
- 输入框与其它筛选控件**等高**(34px 单行高度,多行时框内滚动),"已识别 N 个关键词"显示在标签行右侧并在空间不足时省略,不再把这一格撑成两行高。
筛选区还提供 **重置**:一键清空"签收日期(起/止)"与"关键词"两个输入(排除关键词有自己的「清空排除 / 恢复默认」,不受影响),并提示"已清空签收日期与关键词,填好后点查询"。重置只清输入不立即取数;直接点查询时,空日期按默认区间(本月 1 号 ~ 昨日)、空关键词表示不过滤,因此在跨天且当日快照尚未生成时可能落到实时源(较慢但结果正确)。
筛选区的三个按钮各管一件事,点下去**立即按新条件查询**(互不重复):
| 按钮 | 签收日期 | 关键词 | 排除关键词 | 结果 |
| --- | --- | --- | --- | --- |
| **重置** | 清空(输入框留空) | 清空 | 清空 | 没有任何筛选条件;空日期由服务端兜底为本月 1 号~昨日(`dateRangeCleared` 让输入框保持空白,不被兜底值回填) |
| **恢复默认** | 回到本月 1 号~昨日(显式填回输入框) | 保留 | 恢复系统默认配置(全部勾选) | 默认口径 |
| **清空排除** | 回到本月 1 号~昨日(显式填回输入框) | 保留 | 置空 | 默认区间 + 不排除任何关键词 |
三者过去都只是"改输入、等用户点查询"(重置只清日期与关键词、另两个只改排除词),结果看起来一样容易混;现在各自的语义、界面状态与结果都区分开了,按钮 hover 有对应说明,筛选栏底部也有一行图例。空日期提交时服务端仍按默认区间查询(跨天且当日快照未生成时可能落实时源,较慢但结果正确)。
**默认签收日期区间是「本月 1 号 ~ 昨日」**(由 `normalizeLogisticsArrivalSigningDurationFilters()` 生成):当日签收数据仍在滚动,且每日快照只覆盖到昨日,用"到昨日"作默认值可以让首屏直接命中快照(整区间精确 + 秒级),不再因为区间含当天而回落到实时源。每月 1 号时会自动退化为"昨日当天"(本月 1 号还没到昨日)。

View File

@ -1,6 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react'
import {
buildLogisticsArrivalSigningDurationDashboard,
logisticsArrivalSigningDurationDefaultRange,
serializeExcludeKeywords,
type LogisticsArrivalSigningDurationExcludeKeywords,
type LogisticsArrivalSigningDurationFilters,
type LogisticsArrivalSigningDurationPage,
@ -163,3 +165,56 @@ it('系统管理员在选择器里新增关键词会写入默认排除关键词
global.fetch = originalFetch
}
})
it('重置 / 恢复默认 / 清空排除 三个按钮语义各不相同,并且都立即查询', async () => {
const originalFetch = global.fetch
const dashboard = buildLogisticsArrivalSigningDurationDashboard([row])
global.fetch = jest.fn(async () => ({ ok: true, json: async () => ({ success: true, data: dashboard }) }) as Response) as typeof fetch
const requestSubmit = jest.spyOn(HTMLFormElement.prototype, 'requestSubmit').mockImplementation(() => {})
try {
render(
<LogisticsArrivalSigningDurationView
initialData={initialData}
defaultExcludeKeywords={defaultExcludeKeywords}
canManageExcludeKeywords={false}
canDownload
/>,
)
const dateFrom = document.querySelector<HTMLInputElement>('input[name="dateFrom"]')!
const dateTo = document.querySelector<HTMLInputElement>('input[name="dateTo"]')!
const keyword = screen.getByPlaceholderText(/订单号/)
const hiddenExclude = document.querySelector<HTMLInputElement>('input[name="excludeKeywords"]')!
const hiddenMode = document.querySelector<HTMLInputElement>('input[name="excludeKeywordsMode"]')!
const defaultRange = logisticsArrivalSigningDurationDefaultRange()
// 重置 = 没有任何筛选条件:日期、关键词、排除关键词全部清空
fireEvent.change(dateFrom, { target: { value: '2026-09-10' } })
fireEvent.change(keyword, { target: { value: '桃子' } })
fireEvent.click(screen.getByRole('button', { name: '重置' }))
expect(dateFrom.value).toBe('')
expect(dateTo.value).toBe('')
expect(keyword).toHaveValue('')
expect(hiddenExclude.value).toBe('')
expect(hiddenMode.value).toBe('clear')
expect(requestSubmit).toHaveBeenCalledTimes(1)
// 恢复默认 = 本月1号~昨日 + 系统默认排除关键词
fireEvent.click(screen.getByRole('button', { name: '恢复默认' }))
expect(dateFrom.value).toBe(defaultRange.dateFrom)
expect(dateTo.value).toBe(defaultRange.dateTo)
expect(hiddenMode.value).toBe('default')
expect(hiddenExclude.value).toBe(serializeExcludeKeywords(defaultExcludeKeywords))
expect(requestSubmit).toHaveBeenCalledTimes(2)
// 清空排除 = 本月1号~昨日 + 排除关键词置空(与"重置"的区别是日期区间被显式填回)
fireEvent.click(screen.getByRole('button', { name: '清空排除' }))
expect(dateFrom.value).toBe(defaultRange.dateFrom)
expect(dateTo.value).toBe(defaultRange.dateTo)
expect(hiddenExclude.value).toBe('')
expect(hiddenMode.value).toBe('clear')
expect(requestSubmit).toHaveBeenCalledTimes(3)
} finally {
requestSubmit.mockRestore()
global.fetch = originalFetch
}
})

View File

@ -9,6 +9,7 @@ import {
LOGISTICS_ARRIVAL_SIGNING_DURATION_EXCLUDE_FIELDS,
LOGISTICS_ARRIVAL_SIGNING_DURATION_MAX_EXCLUDE_KEYWORDS,
LOGISTICS_ARRIVAL_SIGNING_DURATION_PAGE_SIZE_OPTIONS,
logisticsArrivalSigningDurationDefaultRange,
mergeExcludeKeywords,
parseExcludeKeywords,
serializeExcludeKeywords,
@ -49,11 +50,14 @@ export function LogisticsArrivalSigningDurationView({
defaultExcludeKeywords,
canManageExcludeKeywords,
canDownload,
dateRangeCleared = false,
}: {
initialData: LogisticsArrivalSigningDurationPage
defaultExcludeKeywords: LogisticsArrivalSigningDurationExcludeKeywords
canManageExcludeKeywords: boolean
canDownload: boolean
/** 「重置」提交过来的 URL 日期是空的:输入框保持空白(查询仍由服务端兜底成默认区间)。 */
dateRangeCleared?: boolean
}) {
const { filters } = initialData
const router = useRouter()
@ -75,14 +79,13 @@ export function LogisticsArrivalSigningDurationView({
const [keywordInput, setKeywordInput] = useState(filters.keyword)
const [dateFromInput, setDateFromInput] = useState(filters.dateFrom)
const [dateToInput, setDateToInput] = useState(filters.dateTo)
const [resetHint, setResetHint] = useState(false)
const keywordCount = splitSearchKeywords(keywordInput).length
/** 重置筛选输入:清空签收日期与关键词(排除关键词有自己的清空/恢复默认),填好后点查询生效。 */
const resetFilterInputs = () => {
/** 「重置」后日期输入框保持空白(服务端查询仍用默认区间兜底),直到用户自己填日期。 */
const [datesCleared, setDatesCleared] = useState(dateRangeCleared)
if (dateRangeCleared && dateFromInput && dateToInput) {
// 首次进入时把服务端兜底的日期清掉,保持"没有任何筛选条件"的观感。
setDateFromInput('')
setDateToInput('')
setKeywordInput('')
setResetHint(true)
}
const [excludeKeywordsMode, setExcludeKeywordsMode] = useState<'default' | 'clear'>(filters.excludeKeywordsMode ?? 'default')
// 系统管理员维护的默认排除关键词(服务端 system config),也是选择器的候选池。
@ -140,12 +143,49 @@ export function LogisticsArrivalSigningDurationView({
const [syncedFiltersKey, setSyncedFiltersKey] = useState(filtersKey)
if (syncedFiltersKey !== filtersKey) {
setSyncedFiltersKey(filtersKey)
setDateFromInput(filters.dateFrom)
setDateToInput(filters.dateTo)
// 「重置」清空全部条件后马上查询,服务端会把空日期兜底成本月 1 号~昨日;
// 这种状态下不要把兜底值回填到输入框,否则看起来"重置没生效"(日期又被填上了)。
if (datesCleared) {
setDateFromInput('')
setDateToInput('')
} else {
setDateFromInput(filters.dateFrom)
setDateToInput(filters.dateTo)
}
setKeywordInput(filters.keyword)
setExcludeInput(serializeExcludeKeywords(filters.excludeKeywords))
setExcludeKeywordsMode(filters.excludeKeywordsMode ?? 'default')
setResetHint(false)
}
/**
* 三个按钮的语义(用户口径):
* - 重置:没有任何筛选条件 —— 日期、关键词、排除关键词全部清空并立即查询(日期交给服务端兜底);
* - 恢复默认:签收日期回到本月 1 号~昨日、排除关键词回到系统默认配置,立即查询;
* - 清空排除:签收日期回到本月 1 号~昨日、排除关键词置空,立即查询。
* 三者过去都只是"改输入、等用户点查询",结果看起来一样;现在各自立即生效且互不重复。
*/
const applyFilterPreset = (preset: 'reset' | 'restore-default' | 'clear-exclude') => {
if (preset === 'reset') {
setDatesCleared(true)
setDateFromInput('')
setDateToInput('')
setKeywordInput('')
setExcludeInput('')
setExcludeKeywordsMode('clear')
} else {
const { dateFrom, dateTo } = logisticsArrivalSigningDurationDefaultRange()
setDatesCleared(false)
setDateFromInput(dateFrom)
setDateToInput(dateTo)
if (preset === 'restore-default') {
setExcludeInput(serializeExcludeKeywords(defaultKeywords))
setExcludeKeywordsMode('default')
} else {
setExcludeInput('')
setExcludeKeywordsMode('clear')
}
}
setPendingFilterSubmit(true)
}
/** 仅系统管理员:把新关键词并入默认排除关键词并落库(服务端同样校验管理员身份)。 */
@ -229,9 +269,9 @@ export function LogisticsArrivalSigningDurationView({
</header>
<form className="sc-logistics-filters" method="get" ref={filterFormRef}>
<label><span>签收日期</span><input type="date" name="dateFrom" value={dateFromInput} onChange={(event) => { setDateFromInput(event.target.value); setResetHint(false) }} /></label>
<label><span>签收日期</span><input type="date" name="dateFrom" value={dateFromInput} onChange={(event) => { setDatesCleared(false); setDateFromInput(event.target.value) }} /></label>
<span className="sc-logistics-range-separator">至</span>
<label><span className="sc-logistics-visually-hidden">签收日期结束</span><input type="date" name="dateTo" value={dateToInput} onChange={(event) => { setDateToInput(event.target.value); setResetHint(false) }} /></label>
<label><span className="sc-logistics-visually-hidden">签收日期结束</span><input type="date" name="dateTo" value={dateToInput} onChange={(event) => { setDatesCleared(false); setDateToInput(event.target.value) }} /></label>
<label className="sc-logistics-search sc-logistics-search-batch">
{/* 高度与其他筛选控件保持一致:单行高度、多行时框内滚动;已识别数量放在标签行,避免把这一格撑高。 */}
<span className="sc-logistics-search-label">
@ -244,7 +284,7 @@ export function LogisticsArrivalSigningDurationView({
name="keyword"
rows={1}
value={keywordInput}
onChange={(event) => { setKeywordInput(event.target.value); setResetHint(false) }}
onChange={(event) => { setKeywordInput(event.target.value) }}
placeholder="订单号 / 物流单号 / 店铺 / 条码 / 采购 / 供应商,支持批量(逗号或换行分隔)"
/>
</div>
@ -269,12 +309,12 @@ export function LogisticsArrivalSigningDurationView({
<input type="hidden" name="page" value="1" />
<input type="hidden" name="pageSize" value={filters.pageSize} />
<div className="sc-logistics-filter-actions">
<button type="button" className="sc-logistics-light-button" title="清空签收日期与关键词输入,填好后点查询生效" onClick={resetFilterInputs}><Eraser size={15} />重置</button>
<button type="button" className="sc-logistics-light-button" onClick={() => { setExcludeInput(''); setExcludeKeywordsMode('clear') }}><X size={15} />清空排除</button>
<button type="button" className="sc-logistics-light-button" onClick={() => { setExcludeInput(serializeExcludeKeywords(defaultKeywords)); setExcludeKeywordsMode('default') }}><RotateCcw size={15} />恢复默认</button>
<button type="button" className="sc-logistics-light-button" title="重置:清空签收日期、关键词与排除关键词,立即查询(日期交给系统默认区间)" onClick={() => applyFilterPreset('reset')}><Eraser size={15} />重置</button>
<button type="button" className="sc-logistics-light-button" title="清空排除:签收日期回到本月 1 号~昨日,排除关键词全部取消,立即查询" onClick={() => applyFilterPreset('clear-exclude')}><X size={15} />清空排除</button>
<button type="button" className="sc-logistics-light-button" title="恢复默认:签收日期回到本月 1 号~昨日,排除关键词恢复系统默认配置,立即查询" onClick={() => applyFilterPreset('restore-default')}><RotateCcw size={15} />恢复默认</button>
<button type="submit"><SlidersHorizontal size={15} />查询</button>
</div>
{resetHint ? <span className="sc-logistics-inline-message sc-logistics-reset-hint" role="status">已清空签收日期与关键词,填好后点查询</span> : null}
<span className="sc-logistics-inline-message sc-logistics-reset-hint" role="status">重置=清空全部条件 · 恢复默认=本月 1 号~昨日+默认排除词 · 清空排除=本月 1 号~昨日+不排除</span>
</form>
{excludePickerOpen ? (

View File

@ -32,6 +32,11 @@ export default async function LogisticsArrivalSigningDurationPage({
const hasExcludeKeywords = Boolean(params && Object.prototype.hasOwnProperty.call(params, 'excludeKeywords'))
const excludeKeywordsMode = first(params?.excludeKeywordsMode) === 'clear' ? 'clear' : 'default'
const submittedExcludeKeywords = first(params?.excludeKeywords).trim()
// 「重置」会以空日期提交(没有筛选条件),服务端再兜底成本月 1 号~昨日;
// 这里把"用户没有填日期"这一事实传给视图,让日期输入框保持空白而不是被兜底值回填。
const dateRangeCleared = Boolean(params && Object.prototype.hasOwnProperty.call(params, 'dateFrom'))
&& !first(params?.dateFrom).trim()
&& !first(params?.dateTo).trim()
const initialData = await logisticsArrivalSigningDurationService.listPage({
dateFrom: first(params?.dateFrom),
dateTo: first(params?.dateTo),
@ -52,6 +57,7 @@ export default async function LogisticsArrivalSigningDurationPage({
defaultExcludeKeywords={defaultExcludeKeywords}
canManageExcludeKeywords={canManageSupplyChainPermissions(authUser)}
canDownload={access.view}
dateRangeCleared={dateRangeCleared}
/>
)
}

View File

@ -565,10 +565,12 @@ export function excludeKeywordsCacheKey(value: LogisticsArrivalSigningDurationEx
.join('|')
}
export function normalizeLogisticsArrivalSigningDurationFilters(
input: Partial<LogisticsArrivalSigningDurationFilters>,
now = new Date(),
): LogisticsArrivalSigningDurationFilters {
/**
* 默认签收日期区间:本月 1 号 ~ 昨日。
* 当日签收数据仍在滚动、且每日快照只覆盖到昨日,用"到昨日"作默认值能让首屏直接命中快照。
* 每月 1 号当天会退化成"昨日当天"(本月 1 号还没到昨日)。
*/
export function logisticsArrivalSigningDurationDefaultRange(now = new Date()) {
const today = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
@ -576,9 +578,18 @@ export function normalizeLogisticsArrivalSigningDurationFilters(
day: '2-digit',
}).format(now)
const monthStart = `${today.slice(0, 7)}-01`
// 默认区间是「本月 1 号 ~ 昨日」:当日签收数据还在滚动,且每日快照只覆盖到昨日。
const yesterday = shiftIsoDate(today, -1)
const defaultFrom = monthStart <= yesterday ? monthStart : yesterday
return {
dateFrom: monthStart <= yesterday ? monthStart : yesterday,
dateTo: yesterday,
}
}
export function normalizeLogisticsArrivalSigningDurationFilters(
input: Partial<LogisticsArrivalSigningDurationFilters>,
now = new Date(),
): LogisticsArrivalSigningDurationFilters {
const { dateFrom: defaultFrom, dateTo: yesterday } = logisticsArrivalSigningDurationDefaultRange(now)
const dateFrom = /^\d{4}-\d{2}-\d{2}$/.test(input.dateFrom ?? '') ? input.dateFrom! : defaultFrom
const dateTo = /^\d{4}-\d{2}-\d{2}$/.test(input.dateTo ?? '') ? input.dateTo! : yesterday
const normalizedFrom = dateFrom <= dateTo ? dateFrom : dateTo