feat(supply-chain): 查询框直接支持批量筛选
This commit is contained in:
parent
c691669101
commit
211aff2578
@ -273,7 +273,7 @@ describe('限价管理', () => {
|
|||||||
expect(screen.getByRole('button', { name: '筛选运营负责人' })).toBeInTheDocument()
|
expect(screen.getByRole('button', { name: '筛选运营负责人' })).toBeInTheDocument()
|
||||||
expect((screen.getByRole('columnheader', { name: /^组合编码/ }) as HTMLElement).style.top).toBe('0px')
|
expect((screen.getByRole('columnheader', { name: /^组合编码/ }) as HTMLElement).style.top).toBe('0px')
|
||||||
fireEvent.click(screen.getByRole('button', { name: '筛选组合编码', exact: true }))
|
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())
|
const columnLabels = screen.getAllByRole('columnheader').map((cell) => (cell.textContent ?? '').replace(/[↕↑↓▾]/g, '').trim())
|
||||||
expect(columnLabels.indexOf('运营负责人')).toBe(columnLabels.indexOf('组合编码') + 1)
|
expect(columnLabels.indexOf('运营负责人')).toBe(columnLabels.indexOf('组合编码') + 1)
|
||||||
expect(screen.queryByRole('button', { name: '导出明细' })).not.toBeInTheDocument()
|
expect(screen.queryByRole('button', { name: '导出明细' })).not.toBeInTheDocument()
|
||||||
@ -304,11 +304,9 @@ describe('限价管理', () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '筛选条码', exact: true }))
|
fireEvent.click(screen.getByRole('button', { name: '筛选条码', exact: true }))
|
||||||
const dialog = await screen.findByRole('dialog', { name: '筛选条码' })
|
const dialog = await screen.findByRole('dialog', { name: '筛选条码' })
|
||||||
fireEvent.click(within(dialog).getByRole('button', { name: '批量查询' }))
|
const search = within(dialog).getByRole('textbox', { name: '搜索条码' })
|
||||||
fireEvent.change(within(dialog).getByRole('textbox', { name: '批量查询条码' }), {
|
fireEvent.change(search, { target: { value: 'BAR-001 BAR-002 BAR-003' } })
|
||||||
target: { value: 'BAR-001\nBAR-002,BAR-003' },
|
fireEvent.keyDown(search, { key: 'Enter' })
|
||||||
})
|
|
||||||
fireEvent.click(within(dialog).getByRole('button', { name: '查询' }))
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const latestProductRequest = (global.fetch as jest.Mock).mock.calls
|
const latestProductRequest = (global.fetch as jest.Mock).mock.calls
|
||||||
|
|||||||
@ -31,8 +31,6 @@ export function FilterOptionPanel({ field, selected, onChange, contextFilters, o
|
|||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [options, setOptions] = useState<string[]>(field.fixedOptions ?? [])
|
const [options, setOptions] = useState<string[]>(field.fixedOptions ?? [])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [batchOpen, setBatchOpen] = useState(false)
|
|
||||||
const [batchText, setBatchText] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (field.fixedOptions) return
|
if (field.fixedOptions) return
|
||||||
@ -92,13 +90,12 @@ export function FilterOptionPanel({ field, selected, onChange, contextFilters, o
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyBatch = () => {
|
const applyBatch = (raw: string) => {
|
||||||
const values = parseBatchFilterValues(batchText)
|
const values = parseBatchFilterValues(raw)
|
||||||
.map((value) => value === '空值' ? PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE : value)
|
.map((value) => value === '空值' ? PRODUCT_ARCHIVE_EMPTY_FILTER_VALUE : value)
|
||||||
if (!values.length) return
|
if (!values.length) return
|
||||||
onChange([...new Set([...selected, ...values])].slice(0, 20000))
|
onChange([...new Set([...selected, ...values])].slice(0, 20000))
|
||||||
setBatchText('')
|
setSearch('')
|
||||||
setBatchOpen(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -106,25 +103,21 @@ export function FilterOptionPanel({ field, selected, onChange, contextFilters, o
|
|||||||
<input
|
<input
|
||||||
className={styles.filterSearch}
|
className={styles.filterSearch}
|
||||||
value={search}
|
value={search}
|
||||||
placeholder={`搜索${field.label}`}
|
placeholder={`搜索${field.label},多个值用空格分隔`}
|
||||||
aria-label={`搜索${field.label}`}
|
aria-label={`搜索${field.label}`}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
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}>
|
<div className={styles.filterOptions} role="listbox" aria-label={field.label}>
|
||||||
{displayOptions.length === 0 && (
|
{displayOptions.length === 0 && (
|
||||||
<div className={styles.filterEmpty}>{loading ? '加载中…' : '没有可选值'}</div>
|
<div className={styles.filterEmpty}>{loading ? '加载中…' : '没有可选值'}</div>
|
||||||
@ -143,10 +136,7 @@ export function FilterOptionPanel({ field, selected, onChange, contextFilters, o
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.filterFooter}>
|
<div className={styles.filterFooter}>
|
||||||
<div className={styles.filterFooterActions}>
|
<button type="button" className={styles.linkButton} onClick={() => onChange([])}>清空</button>
|
||||||
<button type="button" className={styles.linkButton} onClick={() => onChange([])}>清空</button>
|
|
||||||
<button type="button" className={styles.linkButton} onClick={() => setBatchOpen((open) => !open)}>批量查询</button>
|
|
||||||
</div>
|
|
||||||
{onCollapse && (
|
{onCollapse && (
|
||||||
<button type="button" className={styles.linkButton} onClick={onCollapse}>收起</button>
|
<button type="button" className={styles.linkButton} onClick={onCollapse}>收起</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { parseBatchFilterValues } from './limit-filter-batch'
|
import { parseBatchFilterValues } from './limit-filter-batch'
|
||||||
|
|
||||||
describe('parseBatchFilterValues', () => {
|
describe('parseBatchFilterValues', () => {
|
||||||
it('parses pasted values separated by newlines, commas, semicolons and tabs', () => {
|
it('parses pasted values separated by spaces, newlines, commas and semicolons', () => {
|
||||||
expect(parseBatchFilterValues('CODE-A\nCODE-B,CODE-C; CODE-D、CODE-E\tCODE-A')).toEqual([
|
expect(parseBatchFilterValues('CODE-A CODE-B\nCODE-C,CODE-D; CODE-E、CODE-A')).toEqual([
|
||||||
'CODE-A',
|
'CODE-A',
|
||||||
'CODE-B',
|
'CODE-B',
|
||||||
'CODE-C',
|
'CODE-C',
|
||||||
@ -11,7 +11,7 @@ describe('parseBatchFilterValues', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps spaces inside a single value and ignores empty entries', () => {
|
it('collapses repeated spaces and ignores empty entries', () => {
|
||||||
expect(parseBatchFilterValues(' 供应商 A ,, \n供应商 B ')).toEqual(['供应商 A', '供应商 B'])
|
expect(parseBatchFilterValues(' 供应商A ,, \n供应商B ')).toEqual(['供应商A', '供应商B'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const BATCH_SEPARATOR = /[\n\r,,;;、\t]+/
|
const BATCH_SEPARATOR = /[\s,,;;、]+/
|
||||||
|
|
||||||
/** 批量筛选支持换行、逗号、分号、顿号和制表符分隔,结果去重并保持输入顺序。 */
|
/** 批量筛选支持换行、逗号、分号、顿号和制表符分隔,结果去重并保持输入顺序。 */
|
||||||
export function parseBatchFilterValues(raw: string) {
|
export function parseBatchFilterValues(raw: string) {
|
||||||
|
|||||||
@ -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}
|
.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{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}
|
.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}
|
.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{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}
|
.filterOption:hover{background:#f4f4f5}
|
||||||
@ -56,7 +50,6 @@
|
|||||||
.filterOptionLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
.filterOptionLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
.filterEmpty{padding:10px 7px;color:#a1a1aa;font-size:11px}
|
.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}
|
.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{border:0;background:transparent;color:#52525b;font-size:11px;cursor:pointer;padding:2px 4px}
|
||||||
.linkButton:hover{color:#18181b;text-decoration:underline}
|
.linkButton:hover{color:#18181b;text-decoration:underline}
|
||||||
.tableScroll{position:relative;min-height:0;flex:1;overflow:auto;background:#fff}
|
.tableScroll{position:relative;min-height:0;flex:1;overflow:auto;background:#fff}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user