gongxiangp2/components/HeaderTaobaoSearch.tsx
2026-05-07 16:37:22 +08:00

220 lines
9.0 KiB
TypeScript

'use client'
import { useEffect, useMemo, useRef, useState } from 'react'
import TaobaoImageGalleryModal from './TaobaoImageGalleryModal'
import { TaobaoDetailSummary } from '@/lib/taobao-detail-plus'
function extractItemId(value: string): string | null {
const match = value.trim().match(/\d{8,20}/)
return match ? match[0] : null
}
export default function HeaderTaobaoSearch() {
const [keyword, setKeyword] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [result, setResult] = useState<TaobaoDetailSummary | null>(null)
const [isExpanded, setIsExpanded] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null)
const normalizedItemId = useMemo(() => extractItemId(keyword), [keyword])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
// 如果没有正在加载,也没有结果展示,或者用户点击了很远的地方,则收起
if (!isLoading && !result && !error) {
setIsExpanded(false)
}
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [isLoading, result, error])
// 当展开时,自动聚焦输入框
useEffect(() => {
if (isExpanded) {
setTimeout(() => inputRef.current?.focus(), 100)
}
}, [isExpanded])
const handleSearch = async (event?: React.FormEvent) => {
event?.preventDefault()
if (!normalizedItemId) {
setError('请输入正确的商品ID')
setResult(null)
return
}
try {
setIsLoading(true)
setError(null)
const response = await fetch(`/api/taobao-detail-plus?itemId=${encodeURIComponent(normalizedItemId)}`, {
method: 'GET',
cache: 'no-store',
})
const payload = await response.json().catch(() => null)
if (!response.ok || !payload?.success) {
throw new Error(payload?.error?.message || '商品查询失败,请稍后再试')
}
setResult(payload.data)
} catch (searchError) {
setResult(null)
setError(searchError instanceof Error ? searchError.message : '商品查询失败,请稍后再试')
} finally {
setIsLoading(false)
}
}
const handleClear = () => {
setKeyword('')
setError(null)
setResult(null)
// 如果清空后失焦,考虑是否收起
}
const showDropdown = isExpanded && (isLoading || error || result)
return (
<div ref={containerRef} className="relative flex items-center justify-end">
{/* 搜索容器:控制宽度动画 */}
<div
className={`group relative flex items-center overflow-hidden rounded-full transition-all duration-300 ease-in-out ${
isExpanded
? 'w-64 bg-gray-100 ring-2 ring-blue-500/20 dark:bg-gray-800'
: 'w-10 bg-white/10 hover:bg-white/20 dark:bg-gray-800/50 dark:hover:bg-gray-700'
}`}
>
{/* 点击触发图标 (在左侧保持固定) */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className={`flex h-10 w-10 shrink-0 items-center justify-center transition-colors ${
isExpanded ? 'text-blue-500' : 'text-gray-300 dark:text-gray-400'
}`}
type="button"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
{/* 实际输入框:宽度随容器展开 */}
<form onSubmit={handleSearch} className="flex-1 pr-10">
<input
ref={inputRef}
type="text"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onFocus={() => setIsExpanded(true)}
placeholder="粘贴链接或商品ID"
className="w-full border-none bg-transparent py-2.5 text-sm text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-0 dark:text-gray-100 dark:placeholder:text-gray-500"
/>
</form>
{/* 清空按钮 */}
{isExpanded && keyword && (
<button
type="button"
onClick={handleClear}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-gray-200"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* 搜索结果下拉面板 (仅在展开且有状态时显示) */}
{showDropdown && (
<div className="absolute right-0 top-full z-50 mt-3 w-80 origin-top-right overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl animate-in fade-in zoom-in duration-200 dark:border-gray-700 dark:bg-gray-800">
<div className="p-4">
{isLoading && (
<div className="flex items-center justify-center py-6">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500/20 border-t-blue-500" />
</div>
)}
{error && !isLoading && (
<div className="rounded-xl bg-red-50 p-3 text-xs text-red-600 dark:bg-red-900/20 dark:text-red-400">
{error}
</div>
)}
{result && !isLoading && (
<div className="animate-in fade-in slide-in-from-top-2 duration-300">
<div className="flex gap-3">
{result.mainImageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={result.mainImageUrl}
alt=""
className="h-16 w-16 rounded-lg border border-gray-100 object-cover dark:border-gray-700"
/>
) : (
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-700">
<svg className="h-6 w-6 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
)}
<div className="min-w-0 flex-1">
<div className="line-clamp-2 text-sm font-bold text-gray-900 dark:text-gray-100">{result.title}</div>
<div className="mt-1 flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400">
<span className="font-semibold text-orange-500">¥{result.price}</span>
<span>·</span>
<span className="truncate">{result.shopName}</span>
</div>
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-2">
<div className="rounded-lg bg-gray-50 p-2 text-center dark:bg-gray-700/50">
<div className="text-xs text-gray-500">SKU 图片</div>
<div className="text-sm font-bold text-gray-900 dark:text-gray-100">{result.skuImages?.length || 0}</div>
</div>
<div className="rounded-lg bg-gray-50 p-2 text-center dark:bg-gray-700/50">
<div className="text-xs text-gray-500">详情图片</div>
<div className="text-sm font-bold text-gray-900 dark:text-gray-100">{result.descImages?.length || 0}</div>
</div>
</div>
<button
onClick={() => {
setIsModalOpen(true)
// 不关闭展开状态,但可以暂时关闭结果面板(如果需要)
}}
className="mt-4 w-full rounded-xl bg-gray-900 py-2.5 text-sm font-bold text-white transition-all hover:bg-gray-800 active:scale-[0.98] dark:bg-blue-600 dark:hover:bg-blue-700"
>
查看/下载全部图片
</button>
</div>
)}
</div>
<div className="border-t border-gray-100 bg-gray-50/50 px-4 py-2 text-[10px] text-gray-400 dark:border-gray-700 dark:bg-gray-800/50">
支持自动从商品链接中提取 ID
</div>
</div>
)}
{/* 全量图片预览弹窗 */}
{result && (
<TaobaoImageGalleryModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
result={result}
/>
)}
</div>
)
}