吸收数字员工产物生命周期治理

This commit is contained in:
baiyanyun
2026-06-10 10:23:47 +08:00
parent 362f397e37
commit ed2f19d257
15 changed files with 739 additions and 164 deletions

View File

@@ -1,11 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { Download, Eye, FolderOpen } from 'lucide-react';
import { Download, Eye, FolderOpen, History } from 'lucide-react';
import GlassCard from '@/components/digital/GlassCard';
import CardTitleBar from '@/components/digital/CardTitleBar';
import {
getDigitalEmployeeWorkday,
postDigitalEmployeeWorkdayAction,
accessArtifact,
getArtifactDetail,
getArtifactVersions,
setArtifactRetention,
} from '@/lib/api';
import type {
DigitalEmployeeDailyReport,
@@ -185,6 +188,7 @@ function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArt
const [busyKey, setBusyKey] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [preview, setPreview] = useState<{ title: string; text: string } | null>(null);
const [detail, setDetail] = useState<{ artifact: DigitalEmployeeReportArtifactSource; item: any; versions: any[] } | null>(null);
if (artifacts.length === 0) {
return <p className="de-empty-text" style={{ padding: '18px 0 6px' }}></p>;
@@ -217,6 +221,47 @@ function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArt
}
};
const loadDetail = async (artifact: DigitalEmployeeReportArtifactSource) => {
if (!artifact.id || busyKey) return;
setBusyKey(`${artifact.id}:detail`);
setNotice(null);
try {
const [item, versions] = await Promise.all([
getArtifactDetail(artifact.id),
getArtifactVersions(artifact.id),
]);
setDetail({ artifact, item, versions });
} catch (error) {
setNotice(error instanceof Error ? error.message : '读取产物详情失败');
} finally {
setBusyKey(null);
}
};
const runRetention = async (action: 'mark_keep' | 'mark_expire' | 'mark_review' | 'clear_retention') => {
const artifact = detail?.artifact;
if (!artifact?.id || busyKey) return;
setBusyKey(`${artifact.id}:${action}`);
setNotice(null);
try {
const result = await setArtifactRetention(artifact.id, action, { reason: 'digital_employee_artifact_governance' });
if (!result?.ok) {
setNotice(result?.error || '保留策略写入失败');
return;
}
const [item, versions] = await Promise.all([
getArtifactDetail(artifact.id),
getArtifactVersions(artifact.id),
]);
setDetail({ artifact: { ...artifact, retention_status: String(item?.retention_status || artifact.retention_status || 'unmanaged') }, item, versions });
setNotice('保留策略已记录');
} catch (error) {
setNotice(error instanceof Error ? error.message : '保留策略写入失败');
} finally {
setBusyKey(null);
}
};
return (
<>
<div className="de-artifact-source-list" aria-label="日报产物来源">
@@ -232,6 +277,13 @@ function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArt
<button type="button" className="de-icon-btn" disabled={!previewable || busyKey !== null} onClick={() => runAccess(artifact, 'preview')} title="预览" aria-label="预览"><Eye size={15} /></button>
<button type="button" className="de-icon-btn" disabled={!downloadable || busyKey !== null} onClick={() => runAccess(artifact, 'download')} title="下载" aria-label="下载"><Download size={15} /></button>
<button type="button" className="de-icon-btn" disabled={!openable || busyKey !== null} onClick={() => runAccess(artifact, 'open_location')} title="位置" aria-label="位置"><FolderOpen size={15} /></button>
<button type="button" className="de-icon-btn" disabled={busyKey !== null} onClick={() => loadDetail(artifact)} title="版本" aria-label="版本"><History size={15} /></button>
</div>
<div className="de-artifact-source-meta">
<span>{artifact.version || 'local-v1'}</span>
<span>{artifact.retention_status || 'unmanaged'}</span>
{artifact.delivery_status ? <span>{artifact.delivery_status}</span> : null}
{artifact.access?.last_access_status ? <span>{artifact.access.last_access_status}</span> : null}
</div>
{artifact.uri ? <code>{artifact.uri}</code> : <em>{artifact.kind || 'artifact'}</em>}
</div>
@@ -245,6 +297,26 @@ function ArtifactSourceList({ artifacts }: { artifacts: DigitalEmployeeReportArt
<pre>{preview.text}</pre>
</div>
) : null}
{detail ? (
<div className="de-artifact-preview">
<div><strong>{detail.artifact.name}</strong><button type="button" className="de-btn-secondary" onClick={() => setDetail(null)}></button></div>
<div className="de-artifact-retention-actions">
<button type="button" className="de-btn-secondary" disabled={busyKey !== null} onClick={() => runRetention('mark_keep')}></button>
<button type="button" className="de-btn-secondary" disabled={busyKey !== null} onClick={() => runRetention('mark_expire')}></button>
<button type="button" className="de-btn-secondary" disabled={busyKey !== null} onClick={() => runRetention('mark_review')}></button>
<button type="button" className="de-btn-secondary" disabled={busyKey !== null} onClick={() => runRetention('clear_retention')}></button>
</div>
<div className="de-artifact-version-list">
{(detail.versions.length > 0 ? detail.versions : [detail.item]).filter(Boolean).map((item) => (
<div key={String(item.artifact_id || item.version_key || item.created_at)}>
<strong>{String(item.version || 'local-v1')}</strong>
<span>{String(item.delivery_summary?.delivery_status || item.source_label || '未记录')}</span>
<em>{String(item.created_at || '')}</em>
</div>
))}
</div>
</div>
) : null}
</>
);
}