feat(supply-chain): 查询框直接支持批量筛选

This commit is contained in:
居居 2026-10-19 14:44:28 +08:00 committed by WuYongJie
parent c691669101
commit 211aff2578
5 changed files with 25 additions and 44 deletions

View File

@ -273,7 +273,7 @@ describe('限价管理', () => {
expect(screen.getByRole('button', { name: '筛选运营负责人' })).toBeInTheDocument()
expect((screen.getByRole('columnheader', { name: /^组合编码/ }) as HTMLElement).style.top).toBe('0px')
fireEvent.click(screen.getByRole('button', { name: '筛选组合编码', exact: true }))
expect(await screen.findByRole('button', { name: '批量查询' })).toBeInTheDocument()
expect(await screen.findByRole('textbox', { name: '搜索组合编码' })).toBeInTheDocument()
const columnLabels = screen.getAllByRole('columnheader').map((cell) => (cell.textContent ?? '').replace(/[↕↑↓▾]/g, '').trim())
expect(columnLabels.indexOf('运营负责人')).toBe(columnLabels.indexOf('组合编码') + 1)
expect(screen.queryByRole('button', { name: '导出明细' })).not.toBeInTheDocument()
@ -304,11 +304,9 @@ describe('限价管理', () => {
fireEvent.click(screen.getByRole('button', { name: '筛选条码', exact: true }))
const dialog = await screen.findByRole('dialog', { name: '筛选条码' })
fireEvent.click(within(dialog).getByRole('button', { name: '批量查询' }))
fireEvent.change(within(dialog).getByRole('textbox', { name: '批量查询条码' }), {
target: { value: 'BAR-001\nBAR-002,BAR-003' },
})
fireEvent.click(within(dialog).getByRole('button', { name: '查询' }))
const search = within(dialog).getByRole('textbox', { name: '搜索条码' })
fireEvent.change(search, { target: { value: 'BAR-001 BAR-002 BAR-003' } })
fireEvent.keyDown(search, { key: 'Enter' })
await waitFor(() => {
const latestProductRequest = (global.fetch as jest.Mock).mock.calls

View File

@ -31,8 +31,6 @@ export function FilterOptionPanel({ field, selected, onChange, contextFilters, o
const [search, setSearch] = useState('')
const [options, setOptions] = useState<string[]>(field.fixedOptions ?? [])
const [loading, setLoading] = useState(false)
const [batchOpen, setBatchOpen] = useState(false)
const [batchText, setBatchText] = useState('')
useEffect(() => {
if (field.fixedOptions) return
@ -92,13 +90,12 @@ export function FilterOptionPanel({ field, selected, onChange, contextFilters, o
)
}
const applyBatch = () => {
const values = parseBatchFilterValues(batchText)
const applyBatch = (raw: string) => {
const values = parseBatchFilterValues(raw)
.map((value) => value === '空值' ? PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE : value)
if (!values.length) return
onChange([...new Set([...selected, ...values])].slice(0, 20000))
setBatchText('')
setBatchOpen(false)
setSearch('')
}
return (
@ -106,25 +103,21 @@ export function FilterOptionPanel({ field, selected, onChange, contextFilters, o
<input
className={styles.filterSearch}
value={search}
placeholder={`搜索${field.label}`}
placeholder={`搜索${field.label},多个值用空格分隔`}
aria-label={`搜索${field.label}`}
onChange={(event) => setSearch(event.target.value)}
onKeyDown={(event) => {
if (event.key !== 'Enter') return
event.preventDefault()
applyBatch(search)
}}
onPaste={(event) => {
const text = event.clipboardData.getData('text')
if (parseBatchFilterValues(text).length <= 1) return
event.preventDefault()
applyBatch(text)
}}
/>
{batchOpen && (
<div className={styles.batchFilterBox}>
<textarea
className={styles.batchFilterTextarea}
value={batchText}
placeholder={`批量输入${field.label},支持换行、逗号或顿号分隔`}
aria-label={`批量查询${field.label}`}
onChange={(event) => setBatchText(event.target.value)}
/>
<div className={styles.batchFilterActions}>
<button type="button" className={styles.linkButton} onClick={() => setBatchOpen(false)}>取消</button>
<button type="button" className={styles.batchFilterApply} onClick={applyBatch}>查询</button>
</div>
</div>
)}
<div className={styles.filterOptions} role="listbox" aria-label={field.label}>
{displayOptions.length === 0 && (
<div className={styles.filterEmpty}>{loading ? '加载中…' : '没有可选值'}</div>
@ -143,10 +136,7 @@ export function FilterOptionPanel({ field, selected, onChange, contextFilters, o
))}
</div>
<div className={styles.filterFooter}>
<div className={styles.filterFooterActions}>
<button type="button" className={styles.linkButton} onClick={() => onChange([])}>清空</button>
<button type="button" className={styles.linkButton} onClick={() => setBatchOpen((open) => !open)}>批量查询</button>
</div>
<button type="button" className={styles.linkButton} onClick={() => onChange([])}>清空</button>
{onCollapse && (
<button type="button" className={styles.linkButton} onClick={onCollapse}>收起</button>
)}

View File

@ -1,8 +1,8 @@
import { parseBatchFilterValues } from './limit-filter-batch'
describe('parseBatchFilterValues', () => {
it('parses pasted values separated by newlines, commas, semicolons and tabs', () => {
expect(parseBatchFilterValues('CODE-A\nCODE-B,CODE-C; CODE-D、CODE-E\tCODE-A')).toEqual([
it('parses pasted values separated by spaces, newlines, commas and semicolons', () => {
expect(parseBatchFilterValues('CODE-A CODE-B\nCODE-C,CODE-D; CODE-E、CODE-A')).toEqual([
'CODE-A',
'CODE-B',
'CODE-C',
@ -11,7 +11,7 @@ describe('parseBatchFilterValues', () => {
])
})
it('keeps spaces inside a single value and ignores empty entries', () => {
expect(parseBatchFilterValues(' 供应商 A ,, \n供应商 B ')).toEqual(['供应商 A', '供应商 B'])
it('collapses repeated spaces and ignores empty entries', () => {
expect(parseBatchFilterValues(' 供应商A ,, \n供应商B ')).toEqual(['供应商A', '供应商B'])
})
})

View File

@ -1,4 +1,4 @@
const BATCH_SEPARATOR = /[\n\r,,;;、\t]+/
const BATCH_SEPARATOR = /[\s,,;;、]+/
/** 批量筛选支持换行、逗号、分号、顿号和制表符分隔,结果去重并保持输入顺序。 */
export function parseBatchFilterValues(raw: string) {

View File

@ -43,12 +43,6 @@
.filterDropdown{position:absolute;top:calc(100% + 5px);left:0;z-index:35;display:flex;width:236px;max-height:320px;flex-direction:column;border:1px solid #e4e4e7;border-radius:6px;background:#fff;box-shadow:0 14px 36px rgba(24,24,27,.14);padding:6px}
.filterSearch{height:28px;flex:0 0 auto;margin-bottom:5px;border:1px solid #e4e4e7;border-radius:4px;padding:0 8px;color:#27272a;font-size:12px;outline:0}
.filterSearch:focus{border-color:#a1a1aa}
.batchFilterBox{display:flex;flex:0 0 auto;flex-direction:column;gap:6px;border:1px solid #e4e4e7;border-radius:5px;background:#fafafa;margin-bottom:6px;padding:6px}
.batchFilterTextarea{box-sizing:border-box;width:100%;height:58px;resize:vertical;border:1px solid #d4d4d8;border-radius:4px;background:#fff;padding:6px 8px;color:#27272a;font:12px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;outline:0}
.batchFilterTextarea:focus{border-color:#a1a1aa}
.batchFilterActions{display:flex;align-items:center;justify-content:flex-end;gap:8px}
.batchFilterApply{height:26px;border:1px solid #27272a;border-radius:4px;background:#27272a;padding:0 10px;color:#fff;font-size:11px;cursor:pointer}
.batchFilterApply:hover{background:#3f3f46}
.filterOptions{min-height:0;flex:1;overflow-y:auto}
.filterOption{display:flex;min-height:28px;align-items:center;gap:8px;border-radius:4px;padding:0 7px;color:#3f3f46;font-size:12px;cursor:pointer}
.filterOption:hover{background:#f4f4f5}
@ -56,7 +50,6 @@
.filterOptionLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.filterEmpty{padding:10px 7px;color:#a1a1aa;font-size:11px}
.filterFooter{display:flex;flex:0 0 auto;align-items:center;justify-content:space-between;gap:8px;border-top:1px solid #f0f0f1;margin-top:5px;padding-top:6px}
.filterFooterActions{display:flex;align-items:center;gap:4px}
.linkButton{border:0;background:transparent;color:#52525b;font-size:11px;cursor:pointer;padding:2px 4px}
.linkButton:hover{color:#18181b;text-decoration:underline}
.tableScroll{position:relative;min-height:0;flex:1;overflow:auto;background:#fff}