Phase A: index.css 完全重写 [data-theme="light"] block
- 玻璃方向修正: bg-card 从 rgba(0,0,0,0.05) 黑透明 → 双 token 拆分
--color-bg-card: #ffffff 实体白 (admin 卡)
--color-bg-glass: rgba(255,255,255,0.65) 透明白玻璃 (sidebar/modal/banner)
- Aurora 浅色不再 display:none, 改 pastel 紫蓝桃 0.20-0.32 alpha
- Inset highlight 方向反转: 浅色用 rgba(255,255,255,0.50) 白高光 (玻璃顶边标志)
- Backdrop-filter 五档标准: --bf-glass-sm/md/lg/xl (12-40px + saturate 140-180%)
- Multi-layer shadow: --shadow-card-light (2 stops) + --shadow-glass-light (3 stops + inset)
- 暖调 chip: --color-chip-warm-* GitBook 公告风格
- 文字主色: #171823 微紫 → #171717 Vercel Black
Phase B: LandingPage + AuroraCanvas 浅色化
- 移除 LandingPage 的 data-theme="dark" 强制 (V1 的回避)
- LandingPage.module.css 21 处颜色全 var 化
- AuroraCanvas: 订阅 useThemeStore, 新 LIGHT_ORBS 数组 pastel 紫蓝桃,
vignette 浅色用白色, grain opacity 减半
Phase C: 13 个玻璃面升级 (3 sub-agent 并行)
- Modal 类 (Login/ForceChange/VideoDetail.infoPanel/RecordDetail/AssetLibrary/
Announcement/Confirm/TeamsPage.detailModal): 接入 bg-modal-glass +
bf-glass-lg/xl + shadow-glass-light (含 inset highlight)
- Bar/Dropdown/Toast (AnnouncementBanner/Toast/Dropdown/Select/DatePicker):
bg-glass-strong + bf-glass-md + inset-highlight
- Sidebar + 生成页 (Sidebar/PromptInput/GenerationCard): glass + 顶边白高光
- AnnouncementBanner 写双套独立 [data-theme] 规则 (CSS gradient 内不能 var alpha)
Phase D: admin 实体卡 multi-layer shadow (13 处, 1 sub-agent)
- DashboardPage / TeamsPage / UsersPage / RecordsPage / AdminAssetsPage /
LoginRecordsPage / AuditLogsPage / ProfilePage / SettingsPage
的 .statCard / .tableWrapper / .chartWrapper / .accordionItem 等
加 var(--shadow-card-light) 双层柔阴影
AdminLayout 修复 (V1 漏的):
- .layout 改 transparent, 让 AmbientBackground pastel aurora 在主区透出
- .sidebar 加 bf-glass-md + inset highlight + 立体阴影
LoginModal / ForceChangePassword 残留 mint 清理:
- submitBtn bg/border/color 用 mint-accent var, 字重 500→600 + 字距 0.04em
- input:focus border 用 var(--color-mint-accent)
- 加 bf-glass-sm + inset highlight
验证:
- TS 编译过
- vitest 71 fail / 162 pass 与 V1 基线完全一致, 无新增回归
- 24 张 V2 截图位于 docs/screenshots/v2/ (本地, .gitignore 排除 png)
完成报告: docs/todo/亮色主题切换V2-完成报告.md
V2 plan: docs/todo/亮色主题切换V2.md
视觉对齐稿: docs/todo/showcase.html
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
154 lines
6.5 KiB
TypeScript
154 lines
6.5 KiB
TypeScript
import type { AdminRecord } from '../types';
|
||
import { ReferenceList } from './ReferenceList';
|
||
|
||
const STATUS_MAP: Record<string, { label: string; color: string; bg: string }> = {
|
||
completed: { label: '已完成', color: 'var(--color-success)', bg: 'var(--color-success-bg)' },
|
||
failed: { label: '失败', color: 'var(--color-danger)', bg: 'var(--color-danger-bg)' },
|
||
processing: { label: '生成中', color: 'var(--color-info)', bg: 'var(--color-info-bg)' },
|
||
queued: { label: '排队中', color: 'var(--color-info)', bg: 'var(--color-info-bg)' },
|
||
};
|
||
|
||
const MODE_MAP: Record<string, string> = { universal: '全能参考', keyframe: '首尾帧' };
|
||
|
||
interface Props {
|
||
record: AdminRecord;
|
||
onClose: () => void;
|
||
showTeam?: boolean;
|
||
showCost?: boolean;
|
||
}
|
||
|
||
export function RecordDetailModal({ record: r, onClose, showTeam, showCost }: Props) {
|
||
const st = STATUS_MAP[r.status] || STATUS_MAP.processing;
|
||
|
||
const elapsed = (() => {
|
||
if (!r.completed_at) return '-';
|
||
const ms = new Date(r.completed_at).getTime() - new Date(r.created_at).getTime();
|
||
if (ms < 0) return '-';
|
||
const sec = Math.round(ms / 1000);
|
||
if (sec < 60) return `${sec}秒`;
|
||
const min = Math.floor(sec / 60);
|
||
const s = sec % 60;
|
||
return `${min}分${s > 0 ? s + '秒' : ''}`;
|
||
})();
|
||
|
||
const refs = r.reference_urls || [];
|
||
|
||
return (
|
||
<>
|
||
<div style={overlay} onClick={onClose}>
|
||
<div style={modal} onClick={(e) => e.stopPropagation()}>
|
||
{/* Header */}
|
||
<div style={header}>
|
||
<span style={{ fontSize: 16, fontWeight: 600, color: 'var(--color-text-light)' }}>任务详情</span>
|
||
<button style={closeBtn} onClick={onClose}>✕</button>
|
||
</div>
|
||
|
||
<div style={body}>
|
||
{/* Status */}
|
||
<div style={{ marginBottom: 16 }}>
|
||
<span style={{ ...statusBadge, color: st.color, background: st.bg }}>{st.label}</span>
|
||
</div>
|
||
|
||
{/* Error */}
|
||
{r.status === 'failed' && r.error_message && (
|
||
<div style={errorBox}>
|
||
<div style={{ fontWeight: 500, marginBottom: 4 }}>失败原因</div>
|
||
<div>{r.error_message}</div>
|
||
{r.raw_error && r.raw_error !== r.error_message && (
|
||
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--color-text-tertiary)', fontFamily: 'monospace', wordBreak: 'break-all' }}>
|
||
原始错误:{r.raw_error}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Info Grid */}
|
||
<div style={sectionTitle}>基本信息</div>
|
||
<div style={infoGrid}>
|
||
{r.ark_task_id && <InfoItem label="任务ID" value={r.ark_task_id} />}
|
||
{r.username && <InfoItem label="用户" value={r.username} />}
|
||
{showTeam && r.team_name && <InfoItem label="团队" value={r.team_name} />}
|
||
<InfoItem label="提交时间" value={new Date(r.created_at).toLocaleString('zh-CN')} />
|
||
<InfoItem label="耗时" value={elapsed} />
|
||
<InfoItem label="模型" value={r.model === 'seedance_2.0_fast' ? 'AirDrama Fast' : 'AirDrama'} />
|
||
<InfoItem label="模式" value={MODE_MAP[r.mode] || r.mode} />
|
||
<InfoItem label="比例" value={r.aspect_ratio || '-'} />
|
||
<InfoItem label="分辨率" value={r.resolution ? r.resolution.toUpperCase() : '-'} />
|
||
<InfoItem label="时长" value={r.duration != null ? `${r.duration}秒` : '-'} />
|
||
<InfoItem label="Tokens" value={(r.tokens_consumed || 0).toLocaleString()} />
|
||
{showCost && <InfoItem label="费用" value={`¥${(r.cost_amount || 0).toFixed(2)}`} />}
|
||
{r.seed != null && r.seed !== -1 && <InfoItem label="种子值" value={String(r.seed)} />}
|
||
</div>
|
||
|
||
{/* Prompt */}
|
||
<div style={sectionTitle}>提示词</div>
|
||
<div style={promptBox}>{r.prompt || '(无提示词)'}</div>
|
||
|
||
{/* References */}
|
||
{refs.length > 0 && (
|
||
<>
|
||
<div style={sectionTitle}>参考素材({refs.length})</div>
|
||
<ReferenceList references={refs} />
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function InfoItem({ label, value }: { label: string; value: string }) {
|
||
return (
|
||
<div style={{ minWidth: 0 }}>
|
||
<div style={{ fontSize: 11, color: 'var(--color-text-tertiary)', marginBottom: 2 }}>{label}</div>
|
||
<div style={{ fontSize: 13, color: 'var(--color-text-light)', wordBreak: 'break-all' }}>{value}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Styles
|
||
const overlay: React.CSSProperties = {
|
||
position: 'fixed', inset: 0, background: 'var(--color-modal-overlay)', display: 'flex',
|
||
alignItems: 'center', justifyContent: 'center', zIndex: 10000,
|
||
};
|
||
const modal: React.CSSProperties = {
|
||
background: 'var(--color-bg-modal-glass)',
|
||
backdropFilter: 'blur(24px) saturate(180%)',
|
||
WebkitBackdropFilter: 'blur(24px) saturate(180%)',
|
||
border: '1px solid var(--color-border-modal-soft)',
|
||
borderRadius: 12,
|
||
width: 560, maxHeight: '80vh', display: 'flex', flexDirection: 'column',
|
||
boxShadow: 'var(--shadow-glass-light)',
|
||
};
|
||
const header: React.CSSProperties = {
|
||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||
padding: '16px 20px', borderBottom: '1px solid var(--color-border-modal)',
|
||
};
|
||
const closeBtn: React.CSSProperties = {
|
||
background: 'none', border: 'none', color: 'var(--color-text-tertiary)', fontSize: 16, cursor: 'pointer',
|
||
padding: '4px 8px', borderRadius: 4,
|
||
};
|
||
const body: React.CSSProperties = {
|
||
padding: 20, overflowY: 'auto', flex: 1,
|
||
};
|
||
const statusBadge: React.CSSProperties = {
|
||
padding: '4px 12px', borderRadius: 6, fontSize: 13, fontWeight: 500,
|
||
};
|
||
const errorBox: React.CSSProperties = {
|
||
background: 'var(--color-danger-bg-soft)', border: '1px solid var(--color-danger-border)',
|
||
borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 13, color: 'var(--color-danger)',
|
||
};
|
||
const sectionTitle: React.CSSProperties = {
|
||
fontSize: 12, color: 'var(--color-text-tertiary)', fontWeight: 500, marginBottom: 8, marginTop: 16,
|
||
textTransform: 'uppercase', letterSpacing: 1,
|
||
};
|
||
const infoGrid: React.CSSProperties = {
|
||
display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px 16px',
|
||
};
|
||
const promptBox: React.CSSProperties = {
|
||
background: 'var(--color-bg-elevated)', borderRadius: 8, padding: 12, fontSize: 13,
|
||
color: 'var(--color-text-monochrome)', lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
||
maxHeight: 150, overflowY: 'auto',
|
||
};
|