252 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
)
}