V0.1全模块完成: 仓库写入(POST)+回执展开+搜索+草稿管理
This commit is contained in:
parent
59d1161244
commit
17c9230410
|
|
@ -7,8 +7,8 @@ import { ReceiptPage } from "./pages/ReceiptPage";
|
|||
import { SettingsPage } from "./pages/SettingsPage";
|
||||
|
||||
const NAV: { id: Page; label: string; icon: string }[] = [
|
||||
{ id: "home", label: "任务首页", icon: "📋" },
|
||||
{ id: "plan", label: "工单面板", icon: "📐" },
|
||||
{ id: "home", label: "频道首页", icon: "🌊" },
|
||||
{ id: "plan", label: "工单面板", icon: "📋" },
|
||||
{ id: "authorize", label: "授权管理", icon: "🔑" },
|
||||
{ id: "receipt", label: "回执记录", icon: "📝" },
|
||||
{ id: "settings", label: "系统设置", icon: "⚙️" },
|
||||
|
|
@ -31,7 +31,7 @@ export function App() {
|
|||
{page === "home" && <HomePage onNavigate={setPage} />}
|
||||
{page === "plan" && <WorkOrderPage />}
|
||||
{page === "authorize" && <AuthorizePage />}
|
||||
{page === "receipt" && <ReceiptPage />}
|
||||
{page === "receipt" && <ReceiptPage onNavigate={setPage} />}
|
||||
{page === "settings" && <SettingsPage />}
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -158,4 +158,53 @@ export function createDraftWorkOrder(summary: string, scope: string[]): WorkOrde
|
|||
operator: "妈妈 · ICE-GL∞", risk: "low", scope,
|
||||
source: "draft",
|
||||
};
|
||||
}
|
||||
|
||||
// 写入 HLDP 回执到 yeye/receipts/ — 通过 Vite 代理走 Gitea API PUT
|
||||
const WRITE_BASE = "/gitea-api/repos/bingshuo/yeye/contents";
|
||||
|
||||
export async function writeReceiptToRepo(receipt: {
|
||||
receipt_id: string; operation: string; operator: string;
|
||||
timestamp: string; result: string; status: string; scope?: string[];
|
||||
}): Promise<boolean> {
|
||||
const date = receipt.timestamp.split("T")[0];
|
||||
const filename = `receipts/${date}-${receipt.receipt_id.toLowerCase().replace(/\s+/g, "-")}.yml`;
|
||||
const scopeList = receipt.scope?.map(s => ` - "${s}"`).join("\n") || " - \"全部\"";
|
||||
const yml = `receipt_id: "${receipt.receipt_id}"
|
||||
operation: "${receipt.operation}"
|
||||
operator: "${receipt.operator}"
|
||||
timestamp: "${receipt.timestamp}"
|
||||
result: "${receipt.result}"
|
||||
status: "${receipt.status}"
|
||||
scope:
|
||||
${scopeList}
|
||||
`;
|
||||
|
||||
try {
|
||||
const body = {
|
||||
content: btoa(unescape(encodeURIComponent(yml))),
|
||||
message: `HoloLake V0.1: ${receipt.operation}`,
|
||||
branch: "main",
|
||||
};
|
||||
let res = await fetch(`${WRITE_BASE}/${filename}`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res.status === 409) {
|
||||
const existing = await fetchJson(`${WRITE_BASE}/${filename}?ref=main`);
|
||||
res = await fetch(`${WRITE_BASE}/${filename}`, {
|
||||
method: "PUT", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...body, sha: existing.sha }),
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
const errText = await res.text();
|
||||
console.error("写入失败:", res.status, errText);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("写入异常:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
export interface WorkOrder { id: string; summary: string; target: string; scope: string[]; risk: "low"|"medium"|"high"|"critical"; status: "draft"|"planned"|"authorized"|"executing"|"completed"|"closed"; acceptanceCriteria: string[]; createdBy: string; createdAt: string; updatedAt: string; }
|
||||
export interface Receipt { id: string; workOrderId: string; actions: string[]; executor: string; evidence: string[]; result: "success"|"partial"|"failed"; limitations: string[]; completedAt: string; }
|
||||
export type Page = "home"|"plan"|"authorize"|"receipt"|"settings";
|
||||
export type Page = "home"|"plan"|"authorize"|"receipt"|"module"|"settings";
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function HomePage({ onNavigate }: Props) {
|
|||
<div style={{fontWeight:600,fontSize:"0.95rem"}}>工单面板</div>
|
||||
<div style={{fontSize:"0.8rem",color:"var(--t2)"}}>创建 · 追踪 · 闭环</div>
|
||||
</div>
|
||||
<div className="card" style={{cursor:"pointer"}} onClick={() => onNavigate("settings")}>
|
||||
<div className="card" style={{cursor:"pointer"}} onClick={() => onNavigate("module")}>
|
||||
<div style={{fontSize:"1.2rem",marginBottom:"0.25rem"}}>📦</div>
|
||||
<div style={{fontWeight:600,fontSize:"0.95rem"}}>模块商店</div>
|
||||
<div style={{fontSize:"0.8rem",color:"var(--t2)"}}>浏览 · 安装 · 管理</div>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,87 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { fetchReceipts, ApiReceipt } from "../lib/api";
|
||||
import { Page } from "../lib/types";
|
||||
|
||||
export function ReceiptPage() {
|
||||
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("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchReceipts().then(data => { setReceipts(data); setLoading(false); }).catch(() => setLoading(false));
|
||||
}, []);
|
||||
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>
|
||||
|
||||
return <div><h1 className="page-title">📝 回执记录</h1>
|
||||
{loading && <p style={{color:"#999"}}>加载中...</p>}
|
||||
{!loading && receipts.length === 0 && <p style={{color:"#999"}}>暂无回执记录</p>}
|
||||
{receipts.map(rc => <div className="card" key={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>
|
||||
<table><tbody>
|
||||
<tr><th style={{width:"100px"}}>执行者</th><td>{rc.operator}</td></tr>
|
||||
<tr><th>时间</th><td>{rc.timestamp}</td></tr>
|
||||
</tbody></table>
|
||||
</div>)}
|
||||
<div style={{marginTop:"1rem",fontSize:"0.8rem",color:"#999"}}>
|
||||
数据源:guanghutansuohe.com/code/bingshuo/yeye/receipts/
|
||||
{!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>;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { WorkOrder } from "../lib/types";
|
||||
import { fetchWorkOrders, loadDrafts, saveDrafts, createDraftWorkOrder } from "../lib/api";
|
||||
import { fetchWorkOrders, loadDrafts, saveDrafts, createDraftWorkOrder, writeReceiptToRepo } from "../lib/api";
|
||||
|
||||
export function WorkOrderPage() {
|
||||
const [orders, setOrders] = useState<WorkOrder[]>([]);
|
||||
|
|
@ -30,8 +30,34 @@ export function WorkOrderPage() {
|
|||
await load();
|
||||
}
|
||||
|
||||
function handleAuthorize(id: string, decision: "approved" | "rejected") {
|
||||
async function handleAuthorize(id: string, decision: "approved" | "rejected") {
|
||||
setAuthorizations(prev => ({ ...prev, [id]: decision }));
|
||||
const order = orders.find(o => o.id === id);
|
||||
if (decision === "approved" && order) {
|
||||
const success = await writeReceiptToRepo({
|
||||
receipt_id: order.id, operation: order.summary,
|
||||
operator: "小线 · PER-YE-LN-001", timestamp: new Date().toISOString(),
|
||||
result: "success", status: "completed", scope: order.scope,
|
||||
});
|
||||
if (success) {
|
||||
const drafts = loadDrafts().filter(d => d.id !== id);
|
||||
saveDrafts(drafts);
|
||||
await load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteDraft(id: string) {
|
||||
if (!confirm("确认删除这条本地草稿?")) return;
|
||||
const drafts = loadDrafts().filter(d => d.id !== id);
|
||||
saveDrafts(drafts);
|
||||
load();
|
||||
}
|
||||
|
||||
function handleClearAllDrafts() {
|
||||
if (!confirm("确认清空所有本地草稿?")) return;
|
||||
saveDrafts([]);
|
||||
load();
|
||||
}
|
||||
|
||||
const filtered = filter === "all" ? orders : orders.filter(o => o.status === filter);
|
||||
|
|
@ -73,6 +99,7 @@ export function WorkOrderPage() {
|
|||
{f==="all"?"全部":f==="draft"?"草稿":f==="executing"?"执行中":"已完成"} ({counts[f]})
|
||||
</button>
|
||||
))}
|
||||
{counts.draft > 0 && <button className="btn" style={{marginLeft:"auto",fontSize:"0.75rem"}} onClick={handleClearAllDrafts}>🗑️ 清空草稿</button>}
|
||||
</div>
|
||||
|
||||
{loading && <p style={{color:"#999"}}>加载中...</p>}
|
||||
|
|
@ -125,6 +152,11 @@ export function WorkOrderPage() {
|
|||
{needsAuth && authState !== "pending" && <div style={{marginTop:"0.75rem",padding:"0.5rem",background: authState==="approved"?"#dcfce7":"#f3f4f6",borderRadius:"8px",fontSize:"0.85rem"}}>
|
||||
{authState==="approved" ? "✅ 已批准 · 单次执行 · 有效期 24 小时" : "⛔ 已拒绝"}
|
||||
</div>}
|
||||
|
||||
{o.source === "draft" && <div style={{marginTop:"0.75rem"}}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<button className="btn btn-danger" style={{fontSize:"0.75rem"}} onClick={() => handleDeleteDraft(o.id)}>🗑️ 删除草稿</button>
|
||||
</div>}
|
||||
</div>}
|
||||
|
||||
<div style={{fontSize:"0.7rem",color:"var(--t2)",marginTop:"0.5rem"}}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue