278 lines
23 KiB
TypeScript
278 lines
23 KiB
TypeScript
'use client'
|
||
|
||
import Link from 'next/link'
|
||
import { FormEvent, useCallback, useEffect, useRef, useState } from 'react'
|
||
import {
|
||
Activity,
|
||
AlertTriangle,
|
||
CalendarDays,
|
||
CheckCircle2,
|
||
ChevronLeft,
|
||
ChevronRight,
|
||
Clock3,
|
||
RefreshCw,
|
||
Search,
|
||
} from 'lucide-react'
|
||
import DepartmentShell from './DepartmentShell'
|
||
import {
|
||
BUSINESS_TREND_BRANDS,
|
||
type PerformanceConversionSourceSync,
|
||
type PerformanceConversionWorkflowSync,
|
||
type PerformanceConversionWorkflowItem,
|
||
type PerformanceConversionWorkflowList,
|
||
isValidDateKey,
|
||
} from './performance-conversion'
|
||
|
||
type ListState = {
|
||
data: PerformanceConversionWorkflowList | null
|
||
loading: boolean
|
||
error: string | null
|
||
}
|
||
|
||
type WorkflowFilters = {
|
||
query: string
|
||
completedFrom: string
|
||
completedTo: string
|
||
}
|
||
|
||
const PAGE_SIZE = 20
|
||
const EMPTY_FILTERS: WorkflowFilters = { query: '', completedFrom: '', completedTo: '' }
|
||
|
||
function formatDate(value: string | null, includeTime = false) {
|
||
if (!value) return '—'
|
||
const date = /^\d{4}-\d{2}-\d{2}$/.test(value) ? new Date(`${value}T00:00:00+08:00`) : new Date(value)
|
||
if (Number.isNaN(date.getTime())) return '—'
|
||
return new Intl.DateTimeFormat('zh-CN', {
|
||
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
||
...(includeTime ? { hour: '2-digit', minute: '2-digit', hour12: false } : {}),
|
||
}).format(date)
|
||
}
|
||
|
||
function WorkflowSyncStatus({ sync }: { sync: PerformanceConversionWorkflowSync }) {
|
||
const latestSuccess = sync.lastCompletedAt || sync.lastFullReconciledAt
|
||
const latestSuccessLabel = latestSuccess ? formatDate(latestSuccess, true) : null
|
||
const latestReconciliation = sync.lastFullReconciledAt || sync.lastCompletedAt
|
||
const isDelayed = sync.status === 'ready'
|
||
&& latestReconciliation !== null
|
||
&& Date.now() - new Date(latestReconciliation).getTime() > 30 * 60 * 60 * 1000
|
||
|
||
const view = sync.status === 'syncing'
|
||
? { label: '正在同步流程与商品 ID', detail: latestSuccessLabel ? `上次成功 ${latestSuccessLabel}` : '首次同步进行中', tone: 'text-[#0050cb]', icon: RefreshCw, spin: true }
|
||
: sync.status === 'error'
|
||
? { label: '流程与商品 ID 同步异常', detail: latestSuccessLabel ? `暂显示 ${latestSuccessLabel} 的数据` : '暂无可用同步记录', tone: 'text-amber-700', icon: AlertTriangle, spin: false }
|
||
: sync.status === 'pending'
|
||
? { label: '等待首次流程与商品 ID 同步', detail: '后台任务尚未完成', tone: 'text-slate-500', icon: Clock3, spin: false }
|
||
: isDelayed
|
||
? { label: '流程与商品 ID 每日校准延迟', detail: `最近增量成功 ${latestSuccessLabel}`, tone: 'text-amber-700', icon: AlertTriangle, spin: false }
|
||
: { label: '流程与商品 ID 每日增量同步', detail: latestSuccessLabel ? `最近成功 ${latestSuccessLabel}` : '等待首次成功记录', tone: 'text-emerald-700', icon: CheckCircle2, spin: false }
|
||
const Icon = view.icon
|
||
|
||
return (
|
||
<div
|
||
role="status"
|
||
aria-live="polite"
|
||
className={`min-w-0 text-left text-xs leading-5 lg:text-right ${view.tone}`}
|
||
title={latestSuccess ? `最近同步成功:${latestSuccessLabel}(北京时间)` : undefined}
|
||
>
|
||
<p className="inline-flex items-center gap-1.5 font-bold">
|
||
<Icon className={`h-3.5 w-3.5 shrink-0 ${view.spin ? 'animate-spin' : ''}`} aria-hidden="true" />
|
||
<span>{view.label}</span>
|
||
</p>
|
||
<p className="text-slate-500">{view.detail}</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function PerformanceDataSyncStatus({ sync }: { sync?: PerformanceConversionSourceSync }) {
|
||
if (!sync) return null
|
||
const latestSuccess = sync.lastCompletedAt
|
||
const latestSuccessLabel = latestSuccess ? formatDate(latestSuccess, true) : null
|
||
const isDelayed = sync.status === 'ready'
|
||
&& latestSuccess !== null
|
||
&& Date.now() - new Date(latestSuccess).getTime() > 48 * 60 * 60 * 1000
|
||
|
||
const view = sync.status === 'syncing'
|
||
? { label: '正在同步经营数据', detail: latestSuccessLabel ? `上次成功 ${latestSuccessLabel}` : '首次同步进行中', tone: 'text-[#0050cb]', icon: RefreshCw, spin: true }
|
||
: sync.status === 'error'
|
||
? { label: '经营数据同步异常', detail: latestSuccessLabel ? `暂显示 ${latestSuccessLabel} 的数据` : '暂无可用同步记录', tone: 'text-amber-700', icon: AlertTriangle, spin: false }
|
||
: sync.status === 'pending'
|
||
? { label: '等待首次经营数据同步', detail: '后台任务尚未完成', tone: 'text-slate-500', icon: Clock3, spin: false }
|
||
: isDelayed
|
||
? { label: '经营数据每日同步延迟', detail: `最近成功 ${latestSuccessLabel}`, tone: 'text-amber-700', icon: AlertTriangle, spin: false }
|
||
: { label: '经营数据每日增量同步', detail: latestSuccessLabel ? `最近成功 ${latestSuccessLabel}` : '等待首次成功记录', tone: 'text-emerald-700', icon: CheckCircle2, spin: false }
|
||
const Icon = view.icon
|
||
|
||
return (
|
||
<div
|
||
role="status"
|
||
aria-live="polite"
|
||
className={`min-w-0 text-left text-xs leading-5 lg:text-right ${view.tone}`}
|
||
title={latestSuccess ? `最近同步成功:${latestSuccessLabel}(北京时间)` : undefined}
|
||
>
|
||
<p className="inline-flex items-center gap-1.5 font-bold">
|
||
<Icon className={`h-3.5 w-3.5 shrink-0 ${view.spin ? 'animate-spin' : ''}`} aria-hidden="true" />
|
||
<span>{view.label}</span>
|
||
</p>
|
||
<p className="text-slate-500">{view.detail}{sync.lastSourceDate ? ` · 已扫描至 ${formatDate(sync.lastSourceDate)}` : ''}</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function WorkflowTable({ items, detailHref }: {
|
||
items: PerformanceConversionWorkflowItem[]
|
||
detailHref: (item: PerformanceConversionWorkflowItem) => string
|
||
}) {
|
||
return (
|
||
<>
|
||
<div className="hidden overflow-x-auto md:block" tabIndex={0} aria-label="美术流程商品 ID 列表">
|
||
<table className="w-full min-w-[920px] table-fixed text-left text-sm">
|
||
<caption className="sr-only">2026 年 4 月 1 日以后已完结美术流程关联的商品 ID</caption>
|
||
<thead className="bg-slate-50 text-xs font-bold text-slate-500"><tr><th className="w-[18%] px-5 py-3">商品 ID</th><th className="w-[24%] px-4 py-3">审批编号</th><th className="w-[18%] px-4 py-3">完结时间</th><th className="w-[25%] px-4 py-3">监控区间</th><th className="w-[15%] px-4 py-3">状态</th></tr></thead>
|
||
<tbody className="divide-y divide-slate-200">
|
||
{items.map((item) => <tr key={item.key} className="bg-white hover:bg-slate-50"><td className="px-5 py-3"><Link href={detailHref(item)} className="break-all font-mono text-sm font-bold text-[#0050cb] underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]">{item.productId}</Link><p className="mt-1 flex flex-wrap gap-x-2 text-xs text-slate-500">{item.spu && <span className="min-w-0 max-w-full break-all">SPU {item.spu}</span>}<span className="min-w-0 max-w-full break-all">平台 {item.platform || '—'}</span></p></td><td className="px-4 py-3"><p className="break-all font-mono text-xs text-slate-700">{item.approvalNumber || '—'}</p></td><td className="px-4 py-3 tabular-nums text-slate-700">{formatDate(item.completedAt, true)}</td><td className="px-4 py-3 tabular-nums text-slate-700">{item.monitorStartDate} 至 {item.monitorEndDate}</td><td className="px-4 py-3"><span className={`inline-flex min-h-7 items-center rounded-md px-2 text-xs font-bold ${item.monitoringStatus === 'monitoring' ? 'bg-amber-50 text-amber-800 ring-1 ring-inset ring-amber-200' : 'bg-emerald-50 text-emerald-800 ring-1 ring-inset ring-emerald-200'}`}>{item.monitoringStatus === 'monitoring' ? '监控中' : '已满 6 个月'}</span></td></tr>)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<ul className="divide-y divide-slate-200 md:hidden" aria-label="美术流程商品 ID 列表">
|
||
{items.map((item) => <li key={item.key} className="bg-white p-4"><Link href={detailHref(item)} className="block w-full text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"><span className="font-mono text-sm font-bold text-[#0050cb]">{item.productId}</span><span className="mt-1 flex flex-wrap gap-x-2 text-xs text-slate-500">{item.spu && <span className="min-w-0 max-w-full break-all">SPU {item.spu}</span>}<span className="min-w-0 max-w-full break-all">平台 {item.platform || '—'}</span></span><span className="mt-1 block break-all text-xs text-slate-500">审批编号 {item.approvalNumber || '—'}</span><dl className="mt-3 grid grid-cols-2 gap-3 text-xs"><div><dt className="text-slate-500">完结时间</dt><dd className="mt-1 font-semibold text-slate-800">{formatDate(item.completedAt)}</dd></div><div><dt className="text-slate-500">状态</dt><dd className="mt-1 font-semibold text-slate-800">{item.monitoringStatus === 'monitoring' ? '监控中' : '已满 6 个月'}</dd></div><div className="col-span-2"><dt className="text-slate-500">监控区间</dt><dd className="mt-1 font-semibold text-slate-800">{item.monitorStartDate} 至 {item.monitorEndDate}</dd></div></dl></Link></li>)}
|
||
</ul>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default function PerformanceConversionDashboard() {
|
||
const [selectedBrand, setSelectedBrand] = useState<(typeof BUSINESS_TREND_BRANDS)[number]>(BUSINESS_TREND_BRANDS[0])
|
||
const [query, setQuery] = useState('')
|
||
const [completedFrom, setCompletedFrom] = useState('')
|
||
const [completedTo, setCompletedTo] = useState('')
|
||
const [appliedFilters, setAppliedFilters] = useState<WorkflowFilters>(EMPTY_FILTERS)
|
||
const [filterError, setFilterError] = useState<string | null>(null)
|
||
const [page, setPage] = useState(1)
|
||
const [listState, setListState] = useState<ListState>({ data: null, loading: true, error: null })
|
||
const listAbort = useRef<AbortController | null>(null)
|
||
|
||
const updateUrl = useCallback((brand: string, targetPage: number, filters: WorkflowFilters) => {
|
||
const url = new URL(window.location.href)
|
||
url.searchParams.set('brand', brand)
|
||
const filterParams = {
|
||
q: filters.query,
|
||
completedFrom: filters.completedFrom,
|
||
completedTo: filters.completedTo,
|
||
}
|
||
Object.entries(filterParams).forEach(([key, value]) => {
|
||
if (value) url.searchParams.set(key, value)
|
||
else url.searchParams.delete(key)
|
||
})
|
||
if (targetPage > 1) url.searchParams.set('page', String(targetPage))
|
||
else url.searchParams.delete('page')
|
||
url.searchParams.delete('workflowId')
|
||
url.searchParams.delete('productId')
|
||
window.history.replaceState(null, '', url)
|
||
}, [])
|
||
|
||
const loadList = useCallback(async (brand: typeof selectedBrand, targetPage: number, filters: WorkflowFilters) => {
|
||
listAbort.current?.abort()
|
||
const controller = new AbortController()
|
||
listAbort.current = controller
|
||
setListState({ data: null, loading: true, error: null })
|
||
updateUrl(brand, targetPage, filters)
|
||
try {
|
||
const params = new URLSearchParams({ brand, page: String(targetPage), pageSize: String(PAGE_SIZE) })
|
||
if (filters.query) params.set('q', filters.query)
|
||
if (filters.completedFrom) params.set('completedFrom', filters.completedFrom)
|
||
if (filters.completedTo) params.set('completedTo', filters.completedTo)
|
||
const response = await fetch(`/api/department-dashboard/performance-conversion/workflows?${params}`, { cache: 'no-store', signal: controller.signal })
|
||
const payload = await response.json().catch(() => null)
|
||
if (!response.ok || !payload?.success || !payload.data) throw new Error(payload?.error || '业绩转化流程读取失败')
|
||
const data = payload.data as PerformanceConversionWorkflowList
|
||
setListState({ data, loading: false, error: null })
|
||
} catch (error) {
|
||
if ((error as Error).name === 'AbortError') return
|
||
setListState({ data: null, loading: false, error: (error as Error).message || '业绩转化流程读取失败' })
|
||
}
|
||
}, [updateUrl])
|
||
|
||
useEffect(() => {
|
||
const params = new URLSearchParams(window.location.search)
|
||
const brandParam = params.get('brand')
|
||
const initialBrand = BUSINESS_TREND_BRANDS.includes(brandParam as typeof selectedBrand)
|
||
? brandParam as typeof selectedBrand
|
||
: BUSINESS_TREND_BRANDS[0]
|
||
const pageParam = Number.parseInt(params.get('page') || '', 10)
|
||
const initialPage = Number.isSafeInteger(pageParam) && pageParam > 0 ? pageParam : 1
|
||
const initialFilters = {
|
||
query: params.get('q') || '',
|
||
completedFrom: isValidDateKey(params.get('completedFrom')) ? params.get('completedFrom') as string : '',
|
||
completedTo: isValidDateKey(params.get('completedTo')) ? params.get('completedTo') as string : '',
|
||
}
|
||
setSelectedBrand(initialBrand)
|
||
setQuery(initialFilters.query)
|
||
setCompletedFrom(initialFilters.completedFrom)
|
||
setCompletedTo(initialFilters.completedTo)
|
||
setAppliedFilters(initialFilters)
|
||
setPage(initialPage)
|
||
void loadList(initialBrand, initialPage, initialFilters)
|
||
return () => listAbort.current?.abort()
|
||
}, [loadList])
|
||
|
||
const submitSearch = (event: FormEvent) => {
|
||
event.preventDefault()
|
||
if (completedFrom && completedTo && completedFrom > completedTo) {
|
||
setFilterError('完结开始日期不能晚于结束日期')
|
||
return
|
||
}
|
||
const nextFilters = { query: query.trim(), completedFrom, completedTo }
|
||
setFilterError(null)
|
||
setAppliedFilters(nextFilters)
|
||
setPage(1)
|
||
void loadList(selectedBrand, 1, nextFilters)
|
||
}
|
||
|
||
const selectedBrandCount = listState.data?.brands.find((item) => item.brand === selectedBrand)?.count || 0
|
||
const detailHref = useCallback((item: PerformanceConversionWorkflowItem) => {
|
||
const params = new URLSearchParams({
|
||
workflowId: item.workflowId,
|
||
productId: item.productId,
|
||
brand: selectedBrand,
|
||
page: String(page),
|
||
})
|
||
if (appliedFilters.query) params.set('q', appliedFilters.query)
|
||
if (appliedFilters.completedFrom) params.set('completedFrom', appliedFilters.completedFrom)
|
||
if (appliedFilters.completedTo) params.set('completedTo', appliedFilters.completedTo)
|
||
return `/workspaces/business/performance-conversion/detail?${params}`
|
||
}, [appliedFilters, page, selectedBrand])
|
||
|
||
return (
|
||
<DepartmentShell>
|
||
<main className="mx-auto max-w-[1600px] px-4 pb-16 pt-7 sm:px-6 lg:px-8">
|
||
<header className="flex flex-col gap-4 border-b border-[#dfe4ea] pb-6 lg:flex-row lg:items-end lg:justify-between"><div><h1 className="text-3xl font-bold tracking-normal text-[#101828] sm:text-4xl">业绩转化</h1><p className="mt-3 max-w-3xl text-base leading-7 text-[#66758b]">关联 4 月 1 日以后已完结美术申请中的商品 ID,跟踪流程完结后六个月的每日经营变化。</p></div><p className="inline-flex items-center gap-2 self-start text-xs font-semibold text-slate-600 lg:self-auto"><CalendarDays className="h-4 w-4 text-[#9a4d08]" aria-hidden="true" />流程完结日起按日监控</p></header>
|
||
|
||
{listState.error && <div role="alert" className="mt-6 flex flex-col gap-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm font-semibold text-red-800 sm:flex-row sm:items-center sm:justify-between"><span>{listState.error}</span><button type="button" onClick={() => void loadList(selectedBrand, page, appliedFilters)} className="inline-flex min-h-9 items-center justify-center gap-2 rounded-lg border border-red-200 bg-white px-3 text-xs font-bold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500"><RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />重试</button></div>}
|
||
|
||
<div className="mt-6 overflow-hidden rounded-lg border border-slate-200 bg-white lg:grid lg:grid-cols-[176px_minmax(0,1fr)]">
|
||
<aside aria-label="品牌导航" className="border-b border-slate-200 bg-slate-50 lg:border-b-0 lg:border-r"><div className="border-b border-slate-200 px-4 py-4"><p className="text-xs font-bold text-slate-500">美术申请流程</p><p className="mt-1 text-sm font-bold text-slate-900">六品牌</p></div><nav className="flex gap-1 overflow-x-auto p-2 lg:block lg:space-y-1 lg:overflow-visible" aria-label="选择品牌">{BUSINESS_TREND_BRANDS.map((brand) => { const count = listState.data?.brands.find((item) => item.brand === brand)?.count; return <button key={brand} type="button" aria-current={selectedBrand === brand ? 'page' : undefined} onClick={() => { const nextFilters = { ...appliedFilters, query: '' }; setSelectedBrand(brand); setQuery(''); setCompletedFrom(nextFilters.completedFrom); setCompletedTo(nextFilters.completedTo); setAppliedFilters(nextFilters); setFilterError(null); setPage(1); void loadList(brand, 1, nextFilters) }} className={`flex min-h-11 shrink-0 items-center gap-2 rounded-lg px-3 text-left text-sm font-bold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] lg:w-full ${selectedBrand === brand ? 'bg-[#1677ff] text-white' : 'text-slate-600 hover:bg-slate-200/70 hover:text-slate-950'}`}><span className="min-w-0 flex-1 truncate">{brand}</span>{count !== undefined && <span className={`tabular-nums text-xs ${selectedBrand === brand ? 'text-white/80' : 'text-slate-400'}`}>{count}</span>}</button> })}</nav></aside>
|
||
|
||
<section className="min-w-0 bg-white" aria-labelledby="performance-brand-title" aria-busy={listState.loading}>
|
||
<header className="flex flex-col gap-4 border-b border-slate-200 px-5 py-5 sm:px-7 lg:flex-row lg:items-end lg:justify-between"><div><p className="text-xs font-bold text-[#9a4d08]">当前品牌</p><h2 id="performance-brand-title" className="mt-1 text-xl font-bold tracking-normal text-slate-950">{selectedBrand}</h2><p className="mt-1 text-xs text-slate-500">{appliedFilters.completedFrom || appliedFilters.completedTo ? '区间内 ' : ''}{selectedBrandCount} 个有效流程商品 ID{listState.data?.invalidProductIdCount ? ` · ${listState.data.invalidProductIdCount} 条流程未填写有效 ID` : ''}</p></div>{listState.data && <div className="grid gap-2"><WorkflowSyncStatus sync={listState.data.workflowSync} /><PerformanceDataSyncStatus sync={listState.data.performanceSync} /></div>}</header>
|
||
<form onSubmit={submitSearch} className="grid gap-3 border-b border-slate-200 bg-slate-50 px-5 py-4 sm:px-7 md:grid-cols-[minmax(0,1fr)_auto] md:items-end xl:grid-cols-[minmax(280px,0.9fr)_minmax(220px,1fr)_auto]">
|
||
<fieldset className="min-w-0 md:col-span-2 xl:col-span-1">
|
||
<legend className="mb-1 text-xs font-bold text-slate-600">完结日期</legend>
|
||
<div className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2">
|
||
<label className="min-w-0"><span className="sr-only">完结开始日期</span><input type="date" value={completedFrom} max={completedTo || undefined} onChange={(event) => { setCompletedFrom(event.target.value); setFilterError(null) }} className="h-11 w-full min-w-0 rounded-lg border border-slate-300 bg-white px-2 text-sm text-slate-800 outline-none focus:border-[#1677ff] focus-visible:ring-2 focus-visible:ring-[#1677ff]/20" /></label>
|
||
<span className="text-xs font-semibold text-slate-500" aria-hidden="true">至</span>
|
||
<label className="min-w-0"><span className="sr-only">完结结束日期</span><input type="date" value={completedTo} min={completedFrom || undefined} onChange={(event) => { setCompletedTo(event.target.value); setFilterError(null) }} className="h-11 w-full min-w-0 rounded-lg border border-slate-300 bg-white px-2 text-sm text-slate-800 outline-none focus:border-[#1677ff] focus-visible:ring-2 focus-visible:ring-[#1677ff]/20" /></label>
|
||
</div>
|
||
</fieldset>
|
||
<label className="min-w-0"><span className="mb-1 block text-xs font-bold text-slate-600">关键字</span><span className="relative block"><span className="sr-only">搜索商品 ID、审批编号或流程标题</span><Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" aria-hidden="true" /><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索商品 ID、审批编号或流程标题" className="h-11 w-full rounded-lg border border-slate-300 bg-white pl-10 pr-3 text-sm text-slate-900 outline-none focus:border-[#1677ff] focus-visible:ring-2 focus-visible:ring-[#1677ff]/20" /></span></label>
|
||
<button type="submit" className="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg bg-[#1677ff] px-4 text-sm font-bold text-white hover:bg-[#0f67db] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff] focus-visible:ring-offset-2"><Search className="h-4 w-4" aria-hidden="true" />查询</button>
|
||
{filterError && <p role="alert" className="text-xs font-semibold text-red-700 md:col-span-3">{filterError}</p>}
|
||
</form>
|
||
|
||
{listState.loading && !listState.data ? <div role="status" className="px-5 py-16 text-center"><RefreshCw className="mx-auto h-7 w-7 animate-spin text-[#1677ff]" aria-hidden="true" /><p className="mt-4 text-sm font-semibold text-slate-700">正在读取美术流程商品 ID</p></div> : listState.data?.items.length ? <><WorkflowTable items={listState.data.items} detailHref={detailHref} /><div className="flex items-center justify-between border-t border-slate-200 bg-slate-50 px-5 py-3 text-xs text-slate-500 sm:px-7"><span>第 {page} 页 · 共 {listState.data.total} 项</span><div className="flex gap-2"><button type="button" aria-label="上一页" disabled={page <= 1 || listState.loading} onClick={() => { const next = page - 1; setPage(next); void loadList(selectedBrand, next, appliedFilters) }} className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-slate-300 bg-white text-slate-700 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"><ChevronLeft className="h-4 w-4" aria-hidden="true" /></button><button type="button" aria-label="下一页" disabled={!listState.data.hasMore || listState.loading} onClick={() => { const next = page + 1; setPage(next); void loadList(selectedBrand, next, appliedFilters) }} className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-slate-300 bg-white text-slate-700 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"><ChevronRight className="h-4 w-4" aria-hidden="true" /></button></div></div></> : <div className="px-5 py-16 text-center"><Activity className="mx-auto h-7 w-7 text-slate-400" aria-hidden="true" /><p className="mt-3 text-sm font-bold text-slate-700">{appliedFilters.query || appliedFilters.completedFrom || appliedFilters.completedTo ? '该筛选条件下没有匹配的流程商品 ID' : '该品牌暂无已完结且填写有效商品 ID 的美术流程'}</p>{Boolean(listState.data?.invalidProductIdCount) && <p className="mt-1 text-xs text-slate-500">有 {listState.data?.invalidProductIdCount} 条流程填写了空值或无效占位内容</p>}</div>}
|
||
</section>
|
||
</div>
|
||
</main>
|
||
</DepartmentShell>
|
||
)
|
||
}
|