6f4499afff
AI Code Review / AI Code Review (pull_request) Successful in 1m55s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m1s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m6s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m15s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m56s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m56s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m40s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Successful in 3m47s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 17m20s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
- WechatCallback/WechatBindCallback: 删除localStorage state前置校验(后端state store一次性消费兜底,微信内/跨浏览器误杀正常回调);catch透传后端真实detail;处理微信重定向error参数 - Login: 微信登录按钮加ref同步防连点(连点两次state互相覆盖) - 新增api/errors.ts统一错误提取(axios detail/422数组/状态码/网络超时) - useAssetUpload: 失败卡片记录失败阶段+HTTP状态码+OSS XML错误明细;getOrCreateDefaultProject失败独立提示;toast与拦截器去重 - UploadQueuePanel: 失败卡片多行展示完整原因(阶段+错误+安全重试提示) - uploadDedup: 全量哈希阈值256MB→64MB,抽样片段8MB→16MB,避免100~256MB视频整文件读入内存卡死 - WechatOnboarding: 昵称输入框不预填,用户必须自己输入 - 测试: 新增22用例,全量644通过
144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
/**
|
||
* 上传队列面板
|
||
* 展示批量上传中每个文件的独立状态/进度;失败可重试、可移除、可清空已完成。
|
||
* 上传中的素材卡片同时也会出现在素材网格(后端 prepare 预建 asset),
|
||
* 此面板用于展示真实传输进度与失败重试入口。
|
||
*/
|
||
import React from "react"
|
||
import {
|
||
LoadingOutlined,
|
||
CheckCircleFilled,
|
||
CloseCircleFilled,
|
||
ReloadOutlined,
|
||
CloseOutlined,
|
||
} from "@ant-design/icons"
|
||
import type { UploadItem, UploadFailStage } from "../hooks/useAssetUpload"
|
||
import { COMPLETE_RETRY_HINT } from "../hooks/useAssetUpload"
|
||
|
||
export interface UploadQueuePanelProps {
|
||
items: UploadItem[]
|
||
onRetry: (tempId: string) => void
|
||
onRemove: (tempId: string) => void
|
||
onClearFinished: () => void
|
||
}
|
||
|
||
const STATUS_TEXT: Record<UploadItem["status"], string> = {
|
||
preparing: "排队中…",
|
||
uploading: "上传中",
|
||
ingesting: "转码中…",
|
||
done: "已完成",
|
||
error: "上传失败",
|
||
}
|
||
|
||
/** 失败阶段中文名:让用户一眼看到失败发生在哪一步 */
|
||
const FAIL_STAGE_TEXT: Record<UploadFailStage, string> = {
|
||
prepare: "准备上传阶段",
|
||
transfer: "文件传输阶段",
|
||
complete: "确认入库阶段",
|
||
}
|
||
|
||
const UploadQueuePanel: React.FC<UploadQueuePanelProps> = ({
|
||
items,
|
||
onRetry,
|
||
onRemove,
|
||
onClearFinished,
|
||
}) => {
|
||
if (items.length === 0) return null
|
||
const finishedCount = items.filter((it) => it.status === "done").length
|
||
|
||
return (
|
||
<div className="xx-upload-queue">
|
||
<div className="xx-upload-queue-header">
|
||
<span className="xx-upload-queue-title">
|
||
上传任务({items.length}
|
||
{finishedCount > 0 ? `,已完成 ${finishedCount}` : ""})
|
||
</span>
|
||
{finishedCount > 0 && (
|
||
<button type="button" className="xx-link-btn" onClick={onClearFinished}>
|
||
清空已完成
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div className="xx-upload-queue-list">
|
||
{items.map((it) => {
|
||
const isActive = it.status === "preparing" || it.status === "uploading"
|
||
const showProgress = it.status === "uploading" || it.status === "ingesting"
|
||
return (
|
||
<div key={it.tempId} className={`xx-upload-queue-item xx-upload-queue-${it.status}`}>
|
||
<span className="xx-upload-queue-icon">
|
||
{it.status === "done" || it.duplicated ? (
|
||
<CheckCircleFilled style={{ color: "#22c55e" }} />
|
||
) : it.status === "error" ? (
|
||
<CloseCircleFilled style={{ color: "#ef4444" }} />
|
||
) : (
|
||
<LoadingOutlined style={{ color: "var(--primary-color)" }} />
|
||
)}
|
||
</span>
|
||
<div className="xx-upload-queue-body">
|
||
<div className="xx-upload-queue-name" title={it.fileName}>
|
||
{it.fileName}
|
||
</div>
|
||
{showProgress ? (
|
||
<div className="xx-upload-queue-progress">
|
||
<div
|
||
className="xx-upload-queue-progress-bar"
|
||
style={{ width: `${it.status === "ingesting" ? 100 : it.progress}%` }}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
<div className="xx-upload-queue-status">
|
||
{it.duplicated ? "素材已存在,已跳过" : STATUS_TEXT[it.status]}
|
||
{it.status === "preparing" && it.hint ? `(${it.hint})` : ""}
|
||
{it.status === "uploading" ? ` ${it.progress}%` : ""}
|
||
{it.status === "error" && it.failedStage
|
||
? `(${FAIL_STAGE_TEXT[it.failedStage]})`
|
||
: ""}
|
||
</div>
|
||
{it.status === "error" && it.error ? (
|
||
<div className="xx-upload-queue-error-detail" title={it.error}>
|
||
{it.error.split("\n").map((line, idx) =>
|
||
line === COMPLETE_RETRY_HINT ? (
|
||
<div key={idx} className="xx-upload-queue-error-hint">
|
||
{line}
|
||
</div>
|
||
) : (
|
||
<div key={idx}>{line}</div>
|
||
),
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
<span className="xx-upload-queue-actions">
|
||
{it.status === "error" && (
|
||
<button
|
||
type="button"
|
||
className="xx-upload-queue-btn"
|
||
title={
|
||
it.failedStage === "complete" ? "安全重试(只确认,不重新上传)" : "重试上传"
|
||
}
|
||
onClick={() => onRetry(it.tempId)}
|
||
>
|
||
<ReloadOutlined />
|
||
</button>
|
||
)}
|
||
{(it.status === "error" || it.status === "done") && !isActive && (
|
||
<button
|
||
type="button"
|
||
className="xx-upload-queue-btn"
|
||
title="移除"
|
||
onClick={() => onRemove(it.tempId)}
|
||
>
|
||
<CloseOutlined />
|
||
</button>
|
||
)}
|
||
</span>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default UploadQueuePanel
|