ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
150 lines
8.4 KiB
TypeScript
150 lines
8.4 KiB
TypeScript
import React from 'react';
|
|
import { Download, X } from 'lucide-react';
|
|
import { AnimatePresence, motion } from 'motion/react';
|
|
import type { FlowDetailItem, FlowType } from '../api';
|
|
import type { FlowSelection } from '../model';
|
|
|
|
export const FLOW_META: Record<FlowType, { label: string; tone: string; chip: string }> = {
|
|
delivered: { label: '交车', tone: 'text-blue-600 bg-blue-50 border-blue-100', chip: 'bg-blue-50 text-blue-700 border-blue-100' },
|
|
returned: { label: '还车', tone: 'text-orange-600 bg-orange-50 border-orange-100', chip: 'bg-orange-50 text-orange-700 border-orange-100' },
|
|
replaced: { label: '替换交车', tone: 'text-violet-600 bg-violet-50 border-violet-100', chip: 'bg-violet-50 text-violet-700 border-violet-100' },
|
|
};
|
|
|
|
interface FlowDetailModalProps {
|
|
selectedFlow: FlowSelection | null;
|
|
selectedFlowDetails: FlowDetailItem[];
|
|
setSelectedFlow: React.Dispatch<React.SetStateAction<FlowSelection | null>>;
|
|
exportFlowDetails: (rows?: FlowDetailItem[], title?: string) => void;
|
|
}
|
|
export function FlowDetailModal({
|
|
selectedFlow,
|
|
selectedFlowDetails,
|
|
setSelectedFlow,
|
|
exportFlowDetails,
|
|
}: FlowDetailModalProps) {
|
|
const dateLabel = selectedFlow
|
|
? ('date' in selectedFlow ? selectedFlow.date : `${selectedFlow.start} 至 ${selectedFlow.end}`)
|
|
: '';
|
|
return (
|
|
<>
|
|
{/* Flow Detail Modal */}
|
|
<AnimatePresence>
|
|
{selectedFlow && (
|
|
<div className="fixed inset-0 z-[1000] flex items-end justify-center bg-slate-950/45 p-0 backdrop-blur-sm sm:items-center sm:p-4">
|
|
<motion.div
|
|
data-testid="asset-flow-detail-modal"
|
|
initial={{ opacity: 0, y: 28, scale: 0.98 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 20, scale: 0.98 }}
|
|
className="flex max-h-[88vh] w-full flex-col overflow-hidden rounded-t-3xl bg-white shadow-2xl sm:max-w-5xl sm:rounded-3xl"
|
|
>
|
|
<div className="border-b border-slate-100 bg-slate-950 px-4 py-4 text-white sm:px-5">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div className="text-[11px] font-black uppercase tracking-wide text-slate-400">运维签章时间口径 · 单位:车次</div>
|
|
<h3 className="mt-1 text-lg font-black">
|
|
{dateLabel} · {FLOW_META[selectedFlow.type].label}明细
|
|
</h3>
|
|
<div className="mt-1 text-[12px] font-bold text-slate-300">
|
|
共 {selectedFlowDetails.length} 车次,包含车牌、运维签章时间、建单时间、部门、业务负责人和客户
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
aria-label="关闭流转明细"
|
|
onClick={() => setSelectedFlow(null)}
|
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => exportFlowDetails(selectedFlowDetails, `${dateLabel}-${FLOW_META[selectedFlow.type].label}明细`)}
|
|
disabled={selectedFlowDetails.length === 0}
|
|
className="inline-flex h-8 items-center gap-1 rounded-xl bg-white px-3 text-[11px] font-black text-slate-900 transition hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-40"
|
|
>
|
|
<Download size={13} />
|
|
导出当前明细
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-auto bg-slate-50 p-3 sm:p-4">
|
|
<div className="hidden overflow-hidden rounded-2xl border border-slate-100 bg-white lg:block">
|
|
<table className="w-full table-fixed text-left">
|
|
<thead className="bg-slate-50 text-[11px] font-black text-slate-400">
|
|
<tr>
|
|
<th className="w-28 px-3 py-3">车牌</th>
|
|
<th className="w-40 px-3 py-3">运维签章时间</th>
|
|
<th className="w-40 px-3 py-3">建单时间</th>
|
|
<th className="w-36 px-3 py-3">部门</th>
|
|
<th className="w-28 px-3 py-3">业务负责人</th>
|
|
<th className="px-3 py-3">客户</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100 text-[12px] font-bold text-slate-700">
|
|
{selectedFlowDetails.map((item) => (
|
|
<tr key={item.id} className="hover:bg-blue-50/40">
|
|
<td className="px-3 py-3 font-black text-slate-950">{item.plateNumber}{item.type === 'replaced' && <span className="mt-1 block text-[10px] text-violet-600">替换交车</span>}</td>
|
|
<td className="px-3 py-3 text-slate-500">{item.eventTime || '-'}</td>
|
|
<td className="px-3 py-3 text-blue-600">{item.submitTime || '-'}</td>
|
|
<td className="px-3 py-3">{item.department || '-'}</td>
|
|
<td className="px-3 py-3">{item.manager || '-'}</td>
|
|
<td className="px-3 py-3">{item.customerName || '-'}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="space-y-2 lg:hidden">
|
|
{selectedFlowDetails.map((item) => (
|
|
<div key={item.id} className="rounded-2xl border border-slate-100 bg-white p-3 shadow-sm">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div className="text-base font-black text-slate-950">{item.plateNumber}</div>
|
|
<div className={`mt-1 inline-flex rounded-full border px-2 py-0.5 text-[10px] font-black ${FLOW_META[item.type].chip}`}>
|
|
{item.typeLabel}
|
|
</div>
|
|
</div>
|
|
<div className="text-right text-[10px] font-bold text-slate-400">
|
|
<div>建单</div>
|
|
<div className="mt-0.5 text-[11px] text-blue-600">{item.submitTime?.slice(5, 16) || '-'}</div>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 grid grid-cols-2 gap-2 text-[11px]">
|
|
<div className="rounded-xl bg-slate-50 p-2">
|
|
<div className="font-black text-slate-400">运维签章时间</div>
|
|
<div className="mt-1 font-bold text-slate-700">{item.eventTime || '-'}</div>
|
|
</div>
|
|
<div className="rounded-xl bg-slate-50 p-2">
|
|
<div className="font-black text-slate-400">业务负责人</div>
|
|
<div className="mt-1 font-bold text-slate-700">{item.manager || '-'}</div>
|
|
</div>
|
|
<div className="rounded-xl bg-slate-50 p-2">
|
|
<div className="font-black text-slate-400">部门</div>
|
|
<div className="mt-1 font-bold text-slate-700">{item.department || '-'}</div>
|
|
</div>
|
|
<div className="rounded-xl bg-slate-50 p-2">
|
|
<div className="font-black text-slate-400">客户</div>
|
|
<div className="mt-1 line-clamp-2 font-bold text-slate-700">{item.customerName || '-'}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{selectedFlowDetails.length === 0 && (
|
|
<div className="rounded-2xl bg-white px-4 py-10 text-center text-sm font-bold text-slate-400">当前日期暂无明细</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
}
|