feat(logistics-arrival): 重置 / 恢复默认 / 清空排除 三个按钮语义拆开
过去三个按钮都只是"改输入、等用户点查询",而且重置只清日期+关键词、另两个只改排除词, 最终看起来结果一样。现在各自一件事,点下去立即查询: - 重置:签收日期、关键词、排除关键词全部清空 → 没有任何筛选条件 (空日期仍由服务端兜底成本月 1 号~昨日;新增 dateRangeCleared 标记, 让输入框保持空白而不是被兜底值回填) - 恢复默认:签收日期回到本月 1 号~昨日(显式填回输入框)+ 排除关键词恢复系统默认配置 - 清空排除:签收日期回到本月 1 号~昨日 + 排除关键词置空 配套:`logisticsArrivalSigningDurationDefaultRange()` 从过滤器规范化里抽出来共用(单一来源)、 按钮 hover 说明 + 筛选栏底部图例、去掉旧的"填好后点查询"提示、补视图层测试。
This commit is contained in:
parent
787fcb67bb
commit
1b49125ffa
@ -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 号还没到昨日)。
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user