87 lines
4.3 KiB
TypeScript
87 lines
4.3 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
||
import { fetchReceipts, ApiReceipt } from "../lib/api";
|
||
import { Page } from "../lib/types";
|
||
|
||
interface Props { onNavigate?: (page: Page) => void; }
|
||
|
||
export function ReceiptPage({ onNavigate }: Props) {
|
||
const [receipts, setReceipts] = useState<ApiReceipt[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [expanded, setExpanded] = useState<string | null>(null);
|
||
const [search, setSearch] = useState("");
|
||
|
||
async function load() {
|
||
setLoading(true);
|
||
try { const data = await fetchReceipts(); setReceipts(data); } catch {}
|
||
setLoading(false);
|
||
}
|
||
useEffect(() => { load(); }, []);
|
||
|
||
const filtered = search
|
||
? receipts.filter(r => r.id.toLowerCase().includes(search.toLowerCase()) || r.operation.toLowerCase().includes(search.toLowerCase()))
|
||
: receipts;
|
||
|
||
const counts = { all: receipts.length, success: receipts.filter(r => r.result === "success").length, failed: receipts.filter(r => r.result !== "success").length };
|
||
|
||
return <div>
|
||
<h1 className="page-title">📝 回执记录</h1>
|
||
|
||
<div className="stats" style={{gridTemplateColumns:"repeat(3,1fr)"}}>
|
||
<div className="stat-card"><div className="stat-value">{counts.all}</div><div className="stat-label">全部</div></div>
|
||
<div className="stat-card"><div className="stat-value">{counts.success}</div><div className="stat-label">已成功</div></div>
|
||
<div className="stat-card"><div className="stat-value">{counts.failed}</div><div className="stat-label">待处理</div></div>
|
||
</div>
|
||
|
||
<div style={{marginBottom:"1rem"}}>
|
||
<input className="form-input" placeholder="🔍 搜索回执编号或描述..." value={search} onChange={e => setSearch(e.target.value)} />
|
||
</div>
|
||
|
||
{loading && <p style={{color:"#999"}}>加载中...</p>}
|
||
{!loading && filtered.length === 0 && <p style={{color:"#999"}}>{search ? "无匹配回执" : "暂无回执记录"}</p>}
|
||
|
||
{filtered.map(rc => {
|
||
const isExpanded = expanded === rc.id;
|
||
return <div className="card" key={rc.id} style={{cursor:"pointer"}} onClick={() => setExpanded(isExpanded ? null : rc.id)}>
|
||
<div className="card-header">
|
||
<div>
|
||
<div className="card-title">{rc.id}</div>
|
||
<div className="card-subtitle">{rc.operation}</div>
|
||
</div>
|
||
<span className={`badge ${rc.result==="success"?"badge-success":"badge-warning"}`}>
|
||
{rc.result==="success"?"✅ 已成功":"⚠️ 待验证"}
|
||
</span>
|
||
</div>
|
||
|
||
{isExpanded && <div style={{marginTop:"0.75rem",paddingTop:"0.75rem",borderTop:"1px solid var(--bd)"}}>
|
||
<table><tbody>
|
||
<tr><th style={{width:"100px"}}>执行者</th><td>{rc.operator}</td></tr>
|
||
<tr><th>完成时间</th><td>{rc.timestamp}</td></tr>
|
||
<tr><th>状态</th><td>{rc.result === "success" ? "成功闭环 ✅" : "需跟进 ⚠️"}</td></tr>
|
||
<tr><th>关联工单</th><td>
|
||
{onNavigate && <button className="btn" style={{fontSize:"0.75rem",padding:"0.2rem 0.5rem"}} onClick={e => { e.stopPropagation(); onNavigate("plan"); }}>
|
||
查看关联工单 →
|
||
</button>}
|
||
<span style={{fontSize:"0.75rem",color:"var(--t2)",marginLeft:"0.5rem"}}>{rc.id.replace("RC-","WO-")}</span>
|
||
</td></tr>
|
||
<tr><th>证据链</th><td>
|
||
<span className="badge badge-info" style={{fontSize:"0.7rem"}}>yeye 仓库 receipts/ 目录</span>
|
||
<span style={{fontSize:"0.75rem",color:"var(--t2)",marginLeft:"0.25rem"}}>Git 历史可追溯</span>
|
||
</td></tr>
|
||
</tbody></table>
|
||
|
||
<div style={{marginTop:"0.75rem",padding:"0.5rem",background:"var(--bg2)",borderRadius:"8px",fontSize:"0.8rem",color:"var(--t2)"}}>
|
||
<strong>HLDP 回执链</strong> · 工单 → 执行 → 结果 → 证据 · 不可静默覆盖
|
||
</div>
|
||
</div>}
|
||
|
||
<div style={{fontSize:"0.7rem",color:"var(--t2)",marginTop:"0.4rem"}}>
|
||
{isExpanded ? "▲ 收起" : "▼ 展开详情"} · {rc.timestamp?.split("T")[0] || "—"}
|
||
</div>
|
||
</div>;
|
||
})}
|
||
|
||
<div style={{marginTop:"1rem",fontSize:"0.75rem",color:"#999"}}>
|
||
数据源:guanghutansuohe.com/code/bingshuo/yeye/receipts/ · 最后同步 {new Date().toISOString().split("T")[0]}
|
||
</div>
|
||
</div>;
|
||
} |