252 lines
10 KiB
TypeScript
252 lines
10 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useMemo, useState } from 'react'
|
||
import { Search, X } from 'lucide-react'
|
||
import type { ProductArchiveFilters } from '@/lib/supply-chain-product-archive'
|
||
import {
|
||
ARCHIVE_QUICK_FIELD_OPTIONS,
|
||
splitArchiveFilterValues,
|
||
} from './productArchiveToolbarFields'
|
||
|
||
/**
|
||
* 「多项筛选」居中大弹窗:字段池全部字段平铺(字段名 + 输入框),
|
||
* 填了值的字段参与筛选(字段间「且」,同字段逗号多值「或」),
|
||
* 顶部可搜字段名、清空全部,底部取消/确定。
|
||
*/
|
||
export function ProductArchiveMultiFieldDialog({
|
||
filters,
|
||
onConfirm,
|
||
onCancel,
|
||
}: {
|
||
filters: ProductArchiveFilters
|
||
onConfirm: (nextFilters: ProductArchiveFilters) => void
|
||
onCancel: () => void
|
||
}) {
|
||
const [searchText, setSearchText] = useState('')
|
||
const [texts, setTexts] = useState<Record<string, string>>(() => {
|
||
const record = filters as Record<string, unknown>
|
||
const initial: Record<string, string> = {}
|
||
for (const item of ARCHIVE_QUICK_FIELD_OPTIONS) {
|
||
const values = record[item.key]
|
||
if (Array.isArray(values) && values.length > 0) {
|
||
initial[item.key] = values.map((value) => String(value)).join(',')
|
||
}
|
||
}
|
||
return initial
|
||
})
|
||
|
||
useEffect(() => {
|
||
const onKeyDown = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') onCancel()
|
||
}
|
||
document.addEventListener('keydown', onKeyDown)
|
||
return () => document.removeEventListener('keydown', onKeyDown)
|
||
}, [onCancel])
|
||
|
||
const visibleFields = useMemo(() => {
|
||
const keyword = searchText.trim().toLowerCase()
|
||
if (!keyword) return ARCHIVE_QUICK_FIELD_OPTIONS
|
||
return ARCHIVE_QUICK_FIELD_OPTIONS.filter(
|
||
(item) => item.label.toLowerCase().includes(keyword) || item.key.toLowerCase().includes(keyword),
|
||
)
|
||
}, [searchText])
|
||
|
||
const filledCount = useMemo(
|
||
() => ARCHIVE_QUICK_FIELD_OPTIONS.filter((item) => splitArchiveFilterValues(texts[item.key] ?? '').length > 0).length,
|
||
[texts],
|
||
)
|
||
|
||
const setText = (key: string, text: string) => {
|
||
setTexts((current) => ({ ...current, [key]: text }))
|
||
}
|
||
|
||
const clearAll = () => setTexts({})
|
||
|
||
const confirm = () => {
|
||
const next = { ...filters } as ProductArchiveFilters
|
||
const record = next as Record<string, unknown>
|
||
for (const item of ARCHIVE_QUICK_FIELD_OPTIONS) {
|
||
const values = splitArchiveFilterValues(texts[item.key] ?? '')
|
||
if (values.length > 0) record[item.key] = values
|
||
else delete record[item.key]
|
||
}
|
||
onConfirm(next)
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className="multi-dialog-overlay"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-label="多项筛选"
|
||
onPointerDown={(event) => {
|
||
if (event.target === event.currentTarget) onCancel()
|
||
}}
|
||
>
|
||
<div className="multi-dialog">
|
||
<div className="multi-dialog-head">
|
||
<span className="multi-dialog-title">多项筛选</span>
|
||
<button type="button" className="multi-dialog-close" aria-label="关闭" onClick={onCancel}>
|
||
<X size={15} />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="multi-dialog-toolbar">
|
||
<div className="multi-dialog-search">
|
||
<Search size={14} strokeWidth={1.8} />
|
||
<input
|
||
autoFocus
|
||
value={searchText}
|
||
placeholder="搜索字段名,如:供应商 / 仓库 / 标签"
|
||
onChange={(event) => setSearchText(event.target.value)}
|
||
onKeyDown={(event) => {
|
||
if (event.key === 'Enter') event.preventDefault()
|
||
}}
|
||
/>
|
||
{searchText && (
|
||
<button type="button" aria-label="清空字段搜索" onClick={() => setSearchText('')}>
|
||
<X size={13} />
|
||
</button>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
className="multi-dialog-clear"
|
||
onClick={clearAll}
|
||
disabled={filledCount === 0}
|
||
>
|
||
清空全部
|
||
</button>
|
||
</div>
|
||
|
||
<div className="multi-dialog-body">
|
||
<div className="multi-dialog-grid">
|
||
{visibleFields.map((item) => {
|
||
const text = texts[item.key] ?? ''
|
||
return (
|
||
<label key={item.key} className={`multi-dialog-field ${text ? 'has-value' : ''}`}>
|
||
<span className="multi-dialog-field-label" title={item.label}>{item.label}</span>
|
||
<input
|
||
value={text}
|
||
placeholder="多个值用逗号分隔"
|
||
aria-label={`${item.label}筛选值`}
|
||
onChange={(event) => setText(item.key, event.target.value)}
|
||
onKeyDown={(event) => {
|
||
if (event.key === 'Enter') {
|
||
event.preventDefault()
|
||
confirm()
|
||
}
|
||
}}
|
||
/>
|
||
</label>
|
||
)
|
||
})}
|
||
</div>
|
||
{visibleFields.length === 0 && (
|
||
<div className="multi-dialog-state">没有匹配的字段</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="multi-dialog-footer">
|
||
<span className="multi-dialog-count">已填 {filledCount} 项 · 未填写的字段不参与筛选</span>
|
||
<div className="multi-dialog-footer-buttons">
|
||
<button type="button" className="cancel" onClick={onCancel}>取消</button>
|
||
<button type="button" className="confirm" onClick={confirm}>
|
||
确定{filledCount > 0 ? `(${filledCount})` : ''}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style jsx global>{`
|
||
.multi-dialog-overlay {
|
||
position: fixed; inset: 0; z-index: 1000;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: rgba(24, 24, 27, 0.45);
|
||
padding: 24px;
|
||
}
|
||
.multi-dialog {
|
||
display: flex; flex-direction: column;
|
||
width: 880px; max-width: 100%; height: 560px; max-height: 100%;
|
||
background: #fff; border-radius: 8px;
|
||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.22);
|
||
overflow: hidden;
|
||
}
|
||
.multi-dialog-head {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 12px 16px; border-bottom: 1px solid #f4f4f5; flex: 0 0 auto;
|
||
}
|
||
.multi-dialog-title { font-size: 14px; font-weight: 700; color: #18181b; }
|
||
.multi-dialog-close {
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
width: 26px; height: 26px; padding: 0; border: none; border-radius: 5px;
|
||
background: transparent; color: #a1a1aa; cursor: pointer;
|
||
}
|
||
.multi-dialog-close:hover { background: #f4f4f5; color: #52525b; }
|
||
.multi-dialog-toolbar {
|
||
display: flex; align-items: center; gap: 10px;
|
||
padding: 10px 16px 0; flex: 0 0 auto;
|
||
}
|
||
.multi-dialog-search {
|
||
display: flex; align-items: center; gap: 6px; flex: 1; max-width: 360px;
|
||
height: 32px; padding: 0 9px;
|
||
border: 1px solid #d4d4d8; border-radius: 5px; color: #a1a1aa;
|
||
}
|
||
.multi-dialog-search:focus-within { border-color: #52525b; }
|
||
.multi-dialog-search input {
|
||
min-width: 0; flex: 1; height: 100%; padding: 0; border: none; outline: none;
|
||
color: #27272a; font-size: 13px;
|
||
}
|
||
.multi-dialog-search button {
|
||
display: inline-flex; padding: 0; border: none; background: transparent;
|
||
color: #a1a1aa; cursor: pointer;
|
||
}
|
||
.multi-dialog-clear {
|
||
margin-left: auto; height: 30px; padding: 0 10px;
|
||
border: 1px solid #d4d4d8; border-radius: 5px;
|
||
background: #fff; color: #3f3f46; font-size: 12px; cursor: pointer;
|
||
}
|
||
.multi-dialog-clear:hover:not(:disabled) { background: #f4f4f5; }
|
||
.multi-dialog-clear:disabled { color: #a1a1aa; cursor: not-allowed; }
|
||
.multi-dialog-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 12px 16px; }
|
||
.multi-dialog-grid {
|
||
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 18px;
|
||
}
|
||
.multi-dialog-field { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
||
.multi-dialog-field-label {
|
||
flex: 0 0 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
font-size: 12px; font-weight: 600; color: #3f3f46; text-align: right;
|
||
}
|
||
.multi-dialog-field input {
|
||
min-width: 0; flex: 1; height: 30px; padding: 0 8px;
|
||
border: 1px solid #d4d4d8; border-radius: 4px;
|
||
background: #fff; color: #27272a; font-size: 12px; outline: none;
|
||
}
|
||
.multi-dialog-field input:focus { border-color: #52525b; }
|
||
.multi-dialog-field.has-value input { border-color: #2563eb; }
|
||
.multi-dialog-state {
|
||
display: flex; align-items: center; justify-content: center;
|
||
padding: 60px 20px; color: #a1a1aa; font-size: 13px;
|
||
}
|
||
.multi-dialog-footer {
|
||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||
padding: 12px 16px; border-top: 1px solid #f4f4f5; flex: 0 0 auto;
|
||
}
|
||
.multi-dialog-count { color: #71717a; font-size: 12px; }
|
||
.multi-dialog-footer-buttons { display: inline-flex; gap: 8px; }
|
||
.multi-dialog-footer-buttons button {
|
||
height: 32px; padding: 0 18px; border-radius: 5px; font-size: 13px; cursor: pointer;
|
||
}
|
||
.multi-dialog-footer-buttons .cancel { border: 1px solid #d4d4d8; background: #fff; color: #52525b; }
|
||
.multi-dialog-footer-buttons .cancel:hover { background: #f4f4f5; }
|
||
.multi-dialog-footer-buttons .confirm { border: 1px solid #27272a; background: #27272a; color: #fff; }
|
||
.multi-dialog-footer-buttons .confirm:hover { background: #3f3f46; }
|
||
@media (max-width: 960px) {
|
||
.multi-dialog { width: 100%; height: 90vh; }
|
||
.multi-dialog-grid { grid-template-columns: 1fr; }
|
||
}
|
||
`}</style>
|
||
</div>
|
||
)
|
||
}
|