|
@@ -68,14 +68,26 @@ function parseSteps(body: string | any[] | object): WorkflowStep[] {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function Workflows() {
|
|
export function Workflows() {
|
|
|
- const [strategies, setStrategies] = useState<any[]>([]);
|
|
|
|
|
- const [capabilities, setCapabilities] = useState<Record<string, any>>({});
|
|
|
|
|
- const [isLoading, setIsLoading] = useState(true);
|
|
|
|
|
|
|
+ const getCache = () => {
|
|
|
|
|
+ try { const c = localStorage.getItem('workflows_cache_v1'); return c ? JSON.parse(c) : null; } catch(e) { return null; }
|
|
|
|
|
+ };
|
|
|
|
|
+ const saveCache = (newData: any) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const c = localStorage.getItem('workflows_cache_v1');
|
|
|
|
|
+ const parsed = c ? JSON.parse(c) : {};
|
|
|
|
|
+ localStorage.setItem('workflows_cache_v1', JSON.stringify({ ...parsed, ...newData }));
|
|
|
|
|
+ } catch(e) {}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const [strategies, setStrategies] = useState<any[]>(() => getCache()?.strategies || []);
|
|
|
|
|
+ const [capabilities, setCapabilities] = useState<Record<string, any>>(() => getCache()?.capabilities || {});
|
|
|
|
|
+ const [isLoading, setIsLoading] = useState(() => !(getCache()?.strategies?.length > 0));
|
|
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
|
const [offset, setOffset] = useState(0);
|
|
const [offset, setOffset] = useState(0);
|
|
|
const [hasMore, setHasMore] = useState(true);
|
|
const [hasMore, setHasMore] = useState(true);
|
|
|
- const [totalCount, setTotalCount] = useState<number>(0);
|
|
|
|
|
- const [activeCount, setActiveCount] = useState<number>(0);
|
|
|
|
|
|
|
+ const [totalCount, setTotalCount] = useState<number>(() => getCache()?.totalCount || 0);
|
|
|
|
|
+ const [activeCount, setActiveCount] = useState<number>(() => getCache()?.activeCount || 0);
|
|
|
|
|
+ const [searchQuery, setSearchQuery] = useState("");
|
|
|
const LIMIT = 50;
|
|
const LIMIT = 50;
|
|
|
|
|
|
|
|
const loadStrategies = (currentOffset: number, isInit = false) => {
|
|
const loadStrategies = (currentOffset: number, isInit = false) => {
|
|
@@ -84,13 +96,19 @@ export function Workflows() {
|
|
|
getStrategies(LIMIT, currentOffset).then(stratRes => {
|
|
getStrategies(LIMIT, currentOffset).then(stratRes => {
|
|
|
if (stratRes.total !== undefined) {
|
|
if (stratRes.total !== undefined) {
|
|
|
setTotalCount(stratRes.total);
|
|
setTotalCount(stratRes.total);
|
|
|
|
|
+ saveCache({ totalCount: stratRes.total });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const fetched = stratRes.strategies || stratRes.results || (Array.isArray(stratRes) ? stratRes : []);
|
|
const fetched = stratRes.strategies || stratRes.results || (Array.isArray(stratRes) ? stratRes : []);
|
|
|
if (isInit) {
|
|
if (isInit) {
|
|
|
setStrategies(fetched);
|
|
setStrategies(fetched);
|
|
|
|
|
+ saveCache({ strategies: fetched });
|
|
|
} else {
|
|
} else {
|
|
|
- setStrategies(prev => [...prev, ...fetched]);
|
|
|
|
|
|
|
+ setStrategies(prev => {
|
|
|
|
|
+ const newStrats = [...prev, ...fetched];
|
|
|
|
|
+ saveCache({ strategies: newStrats.slice(0, 50) });
|
|
|
|
|
+ return newStrats;
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
if (fetched.length < LIMIT || (stratRes.total && currentOffset + fetched.length >= stratRes.total)) {
|
|
if (fetched.length < LIMIT || (stratRes.total && currentOffset + fetched.length >= stratRes.total)) {
|
|
|
setHasMore(false);
|
|
setHasMore(false);
|
|
@@ -109,6 +127,7 @@ export function Workflows() {
|
|
|
capMap[c.capability_id || c.id || c.capability_name || c.name] = c;
|
|
capMap[c.capability_id || c.id || c.capability_name || c.name] = c;
|
|
|
});
|
|
});
|
|
|
setCapabilities(capMap);
|
|
setCapabilities(capMap);
|
|
|
|
|
+ saveCache({ capabilities: capMap });
|
|
|
loadStrategies(0, true);
|
|
loadStrategies(0, true);
|
|
|
|
|
|
|
|
// Calculate active count without downloading all bodies
|
|
// Calculate active count without downloading all bodies
|
|
@@ -119,6 +138,7 @@ export function Workflows() {
|
|
|
]).then(results => {
|
|
]).then(results => {
|
|
|
const sum = results.reduce((acc, curr) => acc + (curr.total || 0), 0);
|
|
const sum = results.reduce((acc, curr) => acc + (curr.total || 0), 0);
|
|
|
setActiveCount(sum);
|
|
setActiveCount(sum);
|
|
|
|
|
+ saveCache({ activeCount: sum });
|
|
|
}).catch(console.error);
|
|
}).catch(console.error);
|
|
|
}).catch(console.error);
|
|
}).catch(console.error);
|
|
|
}, []);
|
|
}, []);
|
|
@@ -142,6 +162,18 @@ export function Workflows() {
|
|
|
<StatCard title="覆盖能力" value={Object.keys(capabilities).length} subtext="关联的底层原子能力" icon={Cpu} iconBgColor="bg-amber-50" iconColor="text-amber-600" />
|
|
<StatCard title="覆盖能力" value={Object.keys(capabilities).length} subtext="关联的底层原子能力" icon={Cpu} iconBgColor="bg-amber-50" iconColor="text-amber-600" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <div className="bg-white p-4 rounded-2xl border border-slate-100 shadow-sm relative">
|
|
|
|
|
+ <div className="relative flex-1 max-w-xl">
|
|
|
|
|
+ <Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400" />
|
|
|
|
|
+ <input
|
|
|
|
|
+ value={searchQuery}
|
|
|
|
|
+ onChange={e => setSearchQuery(e.target.value)}
|
|
|
|
|
+ placeholder="模糊匹配工序名称或描述..."
|
|
|
|
|
+ className="w-full bg-slate-50 border border-slate-200 text-sm rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all font-medium text-slate-700 placeholder:font-normal"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<div className="space-y-6">
|
|
<div className="space-y-6">
|
|
|
{isLoading && (
|
|
{isLoading && (
|
|
|
<div className="p-12 text-center text-slate-400 font-bold flex flex-col items-center gap-3">
|
|
<div className="p-12 text-center text-slate-400 font-bold flex flex-col items-center gap-3">
|
|
@@ -156,12 +188,28 @@ export function Workflows() {
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
- {strategies.map(strategy => {
|
|
|
|
|
- const steps = parseSteps(strategy.body);
|
|
|
|
|
- // If no specific capability is listed for the steps but the strategy has capabilities overall,
|
|
|
|
|
- // maybe the UI shouldn't imply them for every step, but the user requested:
|
|
|
|
|
- // "最右侧展示每个步骤的原子能力", so we use `step.capabilities`.
|
|
|
|
|
- const stratCaps = strategy.capability_ids || [];
|
|
|
|
|
|
|
+ {(() => {
|
|
|
|
|
+ const filteredStrategies = strategies.filter(s => {
|
|
|
|
|
+ if (!searchQuery) return true;
|
|
|
|
|
+ const q = searchQuery.toLowerCase();
|
|
|
|
|
+ return (s.name || '').toLowerCase().includes(q) || (s.description || '').toLowerCase().includes(q);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!isLoading && strategies.length > 0 && filteredStrategies.length === 0) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="p-12 text-center border-2 border-dashed border-slate-200 rounded-3xl bg-slate-50/50">
|
|
|
|
|
+ <Search size={32} className="mx-auto mb-3 text-slate-300" />
|
|
|
|
|
+ <p className="text-slate-500 font-bold">未找到匹配的工序</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return filteredStrategies.map(strategy => {
|
|
|
|
|
+ const steps = parseSteps(strategy.body);
|
|
|
|
|
+ // If no specific capability is listed for the steps but the strategy has capabilities overall,
|
|
|
|
|
+ // maybe the UI shouldn't imply them for every step, but the user requested:
|
|
|
|
|
+ // "最右侧展示每个步骤的原子能力", so we use `step.capabilities`.
|
|
|
|
|
+ const stratCaps = strategy.capability_ids || [];
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div key={strategy.id} className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden hover:border-indigo-300 transition-colors duration-300">
|
|
<div key={strategy.id} className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden hover:border-indigo-300 transition-colors duration-300">
|
|
@@ -169,25 +217,12 @@ export function Workflows() {
|
|
|
<div>
|
|
<div>
|
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
|
<h2 className="text-lg font-black text-slate-900">{strategy.name || '未命名工序'}</h2>
|
|
<h2 className="text-lg font-black text-slate-900">{strategy.name || '未命名工序'}</h2>
|
|
|
- <StatusBadge status={strategy.status} />
|
|
|
|
|
- <span className="text-[11px] font-mono text-slate-400 bg-white px-2 py-0.5 rounded border border-slate-100">ID: {strategy.id?.substring(0,8)}</span>
|
|
|
|
|
|
|
+ <span className="text-[11px] font-mono text-slate-400 bg-white px-2 py-0.5 rounded border border-slate-100">ID: {strategy.id}</span>
|
|
|
</div>
|
|
</div>
|
|
|
<p className="text-sm text-slate-600 leading-relaxed max-w-3xl">
|
|
<p className="text-sm text-slate-600 leading-relaxed max-w-3xl">
|
|
|
{strategy.description || '无详细描述'}
|
|
{strategy.description || '无详细描述'}
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- {stratCaps.length > 0 && (
|
|
|
|
|
- <div className="flex flex-col items-end gap-2 shrink-0">
|
|
|
|
|
- <span className="text-[10px] font-black text-slate-400 uppercase tracking-wider">关联全量能力</span>
|
|
|
|
|
- <div className="flex flex-wrap gap-1 justify-end max-w-[200px]">
|
|
|
|
|
- {stratCaps.slice(0, 3).map((cid: string) => (
|
|
|
|
|
- <div key={cid} className="w-2 h-2 rounded-full bg-emerald-500" title={capabilities[cid]?.capability_name || capabilities[cid]?.name || cid} />
|
|
|
|
|
- ))}
|
|
|
|
|
- {stratCaps.length > 3 && <span className="text-[10px] text-slate-400 font-bold px-1">+{stratCaps.length - 3}</span>}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- )}
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="p-6 bg-white">
|
|
<div className="p-6 bg-white">
|
|
@@ -247,8 +282,9 @@ export function Workflows() {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
- })}
|
|
|
|
|
- {hasMore && strategies.length > 0 && (
|
|
|
|
|
|
|
+ });
|
|
|
|
|
+ })()}
|
|
|
|
|
+ {hasMore && strategies.length > 0 && !searchQuery && (
|
|
|
<div className="flex justify-center pt-4">
|
|
<div className="flex justify-center pt-4">
|
|
|
<button
|
|
<button
|
|
|
onClick={handleLoadMore}
|
|
onClick={handleLoadMore}
|