Files
xiaoxia-saas/apps/web/src/pages/assets/components/UploadQueuePanel.tsx
T
xiaoxia 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
fix(#1718/#1714): 微信回调state误杀修复+错误透传防连点、上传失败完整可观测、哈希阈值降至64MB
- 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通过
2026-09-05 20:12:47 +08:00

144 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 上传队列面板
* 展示批量上传中每个文件的独立状态/进度;失败可重试、可移除、可清空已完成。
* 上传中的素材卡片同时也会出现在素材网格(后端 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