初始化
This commit is contained in:
1 parent
34474bd5fc
commit
b638b541d0
3008 files changed
+837699
No files matched your search
@@ -0,0 +1,242 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { WritingStatus } from '../../types/ai-writing';
|
||||
|
||||
interface WritingSkeletonCardProps {
|
||||
status: WritingStatus;
|
||||
/** 素材收集阶段的实时进度文案(仅 researching 阶段使用) */
|
||||
researchProgress?: string;
|
||||
/** 用户自带大纲时为 true:大纲阶段文案显示「整理你的大纲」而非「规划大纲」。 */
|
||||
organizing?: boolean;
|
||||
}
|
||||
|
||||
// ── 基础骨架元素 ──────────────────────────────────────────────────────────────
|
||||
|
||||
function SkeletonLine({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className={cn('h-3 rounded-md bg-muted animate-pulse', className)} />
|
||||
);
|
||||
}
|
||||
|
||||
function ThinkingDots({ color = 'bg-blue-400' }: { color?: string }) {
|
||||
return (
|
||||
<span className="inline-flex gap-1 items-center">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={cn('size-1.5 rounded-full animate-bounce', color)}
|
||||
style={{ animationDelay: `${i * 150}ms` }}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 素材收集骨架屏 ────────────────────────────────────────────────────────────
|
||||
|
||||
function MaterialsSkeleton({ researchProgress }: { researchProgress?: string }) {
|
||||
return (
|
||||
<div className="space-y-3 p-4">
|
||||
{/* 标题行:展示素材收集专家的实时进度 */}
|
||||
<div className="flex items-center gap-2 text-xs text-blue-500 font-medium">
|
||||
<ThinkingDots color="bg-blue-400" />
|
||||
<span>{researchProgress || '素材收集专家搜索中…'}</span>
|
||||
</div>
|
||||
|
||||
{/* 关键词标签占位 */}
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{[60, 80, 50].map((w, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-5 rounded-full bg-blue-100/60 dark:bg-blue-900/30 animate-pulse"
|
||||
style={{ width: `${w}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 素材卡片占位 */}
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div key={i} className="rounded-lg border bg-muted/30 p-3 space-y-2">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-start gap-2">
|
||||
<SkeletonLine className={cn('flex-1 h-3.5', i % 3 === 0 ? 'w-4/5' : i % 3 === 1 ? 'w-3/4' : 'w-2/3')} />
|
||||
</div>
|
||||
{/* 来源 + 时间 + 相关度 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<SkeletonLine className="w-20 h-2" />
|
||||
<SkeletonLine className="w-16 h-2" />
|
||||
<div className="flex-1 h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-blue-200/60 dark:bg-blue-700/40 animate-pulse"
|
||||
style={{ width: `${55 + i * 8}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 摘要行 */}
|
||||
<SkeletonLine className="w-full h-2" />
|
||||
<SkeletonLine className={cn('h-2', i % 2 === 0 ? 'w-5/6' : 'w-4/5')} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 大纲规划骨架屏 ────────────────────────────────────────────────────────────
|
||||
|
||||
function OutlineSkeleton({ organizing }: { organizing?: boolean } = {}) {
|
||||
return (
|
||||
<div className="space-y-3 p-4">
|
||||
<div className="flex items-center gap-2 text-xs text-purple-500 font-medium">
|
||||
<ThinkingDots color="bg-purple-400" />
|
||||
<span>{organizing ? '作家整理你的大纲中…' : '作家规划大纲中…'}</span>
|
||||
</div>
|
||||
|
||||
{/* 文章标题占位 */}
|
||||
<SkeletonLine className="w-3/5 h-4 mx-auto" />
|
||||
|
||||
{/* 章节列表 */}
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-start gap-2 pl-1">
|
||||
<div className="mt-1 size-2 shrink-0 rounded-sm bg-purple-200/70 dark:bg-purple-800/40 animate-pulse" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<SkeletonLine className={cn('h-3', i % 2 === 0 ? 'w-2/5' : 'w-1/2')} />
|
||||
<SkeletonLine className="w-3/5 h-2" />
|
||||
{i % 2 === 0 && <SkeletonLine className="w-2/5 h-2" />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 正文写作骨架屏 ────────────────────────────────────────────────────────────
|
||||
|
||||
function DraftSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="flex items-center gap-2 text-xs text-purple-500 font-medium">
|
||||
<ThinkingDots color="bg-purple-400" />
|
||||
<span>作家写作中…</span>
|
||||
</div>
|
||||
|
||||
{/* 标题占位 */}
|
||||
<SkeletonLine className="w-2/5 h-5 mx-auto" />
|
||||
|
||||
{/* 章节内容 */}
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="space-y-1.5">
|
||||
<SkeletonLine className="w-1/4 h-3.5" />
|
||||
<SkeletonLine className="w-full h-2" />
|
||||
<SkeletonLine className="w-full h-2" />
|
||||
<SkeletonLine className={cn('h-2', i % 2 === 0 ? 'w-4/5' : 'w-11/12')} />
|
||||
<SkeletonLine className="w-full h-2" />
|
||||
<SkeletonLine className="w-3/4 h-2" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 编辑审核骨架屏 ────────────────────────────────────────────────────────────
|
||||
|
||||
function ReviewSkeleton() {
|
||||
return (
|
||||
<div className="space-y-3 p-4">
|
||||
<div className="flex items-center gap-2 text-xs text-orange-500 font-medium">
|
||||
<ThinkingDots color="bg-orange-400" />
|
||||
<span>编辑审核中…</span>
|
||||
</div>
|
||||
|
||||
{/* 综合评分占位 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<SkeletonLine className="w-24 h-3" />
|
||||
<SkeletonLine className="w-12 h-4" />
|
||||
</div>
|
||||
|
||||
{/* 分项评分条 */}
|
||||
{['事实准确度', '逻辑结构', '语言质量'].map((label, i) => (
|
||||
<div key={label} className="space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
<SkeletonLine className="w-8 h-3" />
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-orange-200/60 dark:bg-orange-700/40 animate-pulse"
|
||||
style={{ width: `${55 + i * 10}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 意见条目占位 */}
|
||||
<div className="space-y-2 pt-1 border-t">
|
||||
<SkeletonLine className="w-full h-2" />
|
||||
<SkeletonLine className="w-5/6 h-2" />
|
||||
<SkeletonLine className="w-full h-2" />
|
||||
<SkeletonLine className="w-4/5 h-2" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 按意见修改骨架屏 ──────────────────────────────────────────────────────────
|
||||
|
||||
function RevisingSkeleton() {
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
<div className="flex items-center gap-2 text-xs text-purple-500 font-medium">
|
||||
<ThinkingDots color="bg-purple-400" />
|
||||
<span>作家按意见修改中…</span>
|
||||
</div>
|
||||
<SkeletonLine className="w-2/5 h-5 mx-auto" />
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="space-y-1.5">
|
||||
<SkeletonLine className="w-1/4 h-3.5" />
|
||||
<SkeletonLine className="w-full h-2" />
|
||||
<SkeletonLine className={cn('h-2', i % 2 === 0 ? 'w-4/5' : 'w-full')} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 状态映射 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// researching / awaiting_material_confirm 由 WritingSkeletonCard 单独处理(需透传进度文案)
|
||||
const SKELETON_MAP: Partial<Record<WritingStatus, React.FC>> = {
|
||||
// 实际运行状态
|
||||
writing_outline: OutlineSkeleton,
|
||||
writing_draft: DraftSkeleton,
|
||||
reviewing: ReviewSkeleton,
|
||||
revising: RevisingSkeleton,
|
||||
// 过渡等待状态(用户提交后、后端事件到达前的空白期)
|
||||
awaiting_outline_confirm: OutlineSkeleton, // 过渡到写作
|
||||
awaiting_draft_confirm: DraftSkeleton, // 提交修改意见或编辑审核后
|
||||
awaiting_review_confirm: RevisingSkeleton, // 采纳编辑意见后
|
||||
};
|
||||
|
||||
export function WritingSkeletonCard({ status, researchProgress, organizing }: WritingSkeletonCardProps) {
|
||||
// 素材收集阶段单独渲染,以便透传实时进度文案
|
||||
if (status === 'analyzing_sample' || status === 'researching' || status === 'awaiting_material_confirm') {
|
||||
return (
|
||||
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
|
||||
<MaterialsSkeleton researchProgress={status === 'analyzing_sample' ? '正在分析样文风格…' : researchProgress} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 大纲阶段单独渲染,以便透传「整理 / 规划」文案(用户自带大纲走「整理」)
|
||||
if (status === 'writing_outline' || status === 'awaiting_outline_confirm') {
|
||||
return (
|
||||
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
|
||||
<OutlineSkeleton organizing={organizing} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const Skeleton = SKELETON_MAP[status];
|
||||
if (!Skeleton) return null;
|
||||
return (
|
||||
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
|
||||
<Skeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user