fix: 前端统一解析后端 detail 报错,杜绝 [object Object]
各处 catch 里直接 `toast(err?.response?.data?.detail ?? "xx失败")` 有坑: 请求体校验失败(422)时 detail 是数组,React 会把每个对象渲染成 [object Object]。 - utils/errorMessage.ts(新增): extractErrorMessage 统一解析三种形态 —— 字符串 / FastAPI 校验错误数组 / 对象;数组取 msg 并剥掉 Pydantic 的 "Value error, " 前缀、带上字段名、同字段去重后用「;」拼接;解析不出内容 则回落到调用方给的兜底文案。行为与移动端 extractErrorDetail 对齐 - 19 处调用点全部替换:AdminTasksPage(4) / AdminProductsPage(5) / TaskTreeViewer(4) / AdminPrintConfigPage(2) / ScanPage / CreateProductDialog / ImageUploader / authApi - TaskRejectPayload.images 与 taskApi.rejectTask 的注释同步为「选填,支持空数组」 - 新增 components/ui/ImageUploader.tsx(驳回弹窗选图上传组件), RejectModal 同步放开无图拦截:去掉 images 非空校验、提交按钮不再因 无图禁用、标签由必填星号改为「(选填,最多 9 张)」,并移除因此失效的 error 状态 - AdminTasksPage.tsx: 支持大屏下钻的 ?search / ?status / ?stage 预设参数, 新增「售后流转中」状态 Tab(按 lifecycle_phase 判定)与全局重置; ?stage 只在跳入时生效一次,用户手动改条件即解除锁定 - 杂项清理: TaskFlowView 移除透传的 assigneeNames、TaskListCard/MyTasksPage 移除未使用导入、QrScanner 关闭 verbose 日志
This commit is contained in:
@ -14,6 +14,7 @@ import {
|
||||
import { useToast } from "../../components/ui/Toast";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { getStatusConfig, lifecycleBadge } from "../../constants/task";
|
||||
import { extractErrorMessage } from "../../utils/errorMessage";
|
||||
|
||||
const QR_BASE = "/api/v1/products/qrcode";
|
||||
|
||||
@ -125,12 +126,12 @@ export default function AdminProductsPage() {
|
||||
}
|
||||
|
||||
// ---- 打印 ----
|
||||
async function handleOpenPrint(product: ProductResponse) { /* unchanged */ setPrintTarget(product); setPreviewUrl(null); setPrintCopies(1); setPrintLoading(true); try { setPreviewUrl(await getLabelPreview({ serial_number: product.serial_number, material_name: product.material_name ?? product.material_id ?? "", spec_model: product.spec_model ?? "", order_no: product.order_no ?? "" })); } catch (err: any) { toast(err?.response?.data?.detail ?? "生成预览失败", "error"); setPrintTarget(null); } finally { setPrintLoading(false); } }
|
||||
async function handleConfirmPrint() { if (!printTarget) return; setPrinting(true); try { const r = await executePrint({ serial_number: printTarget.serial_number, material_name: printTarget.material_name ?? "", spec_model: printTarget.spec_model ?? "", order_no: printTarget.order_no ?? "", copies: printCopies }); toast(r.message, "success"); setPrintTarget(null); } catch (err: any) { toast(err?.response?.data?.detail ?? "打印失败", "error"); } finally { setPrinting(false); } }
|
||||
async function handleOpenPrint(product: ProductResponse) { /* unchanged */ setPrintTarget(product); setPreviewUrl(null); setPrintCopies(1); setPrintLoading(true); try { setPreviewUrl(await getLabelPreview({ serial_number: product.serial_number, material_name: product.material_name ?? product.material_id ?? "", spec_model: product.spec_model ?? "", order_no: product.order_no ?? "" })); } catch (err: unknown) { toast(extractErrorMessage(err, "生成预览失败"), "error"); setPrintTarget(null); } finally { setPrintLoading(false); } }
|
||||
async function handleConfirmPrint() { if (!printTarget) return; setPrinting(true); try { const r = await executePrint({ serial_number: printTarget.serial_number, material_name: printTarget.material_name ?? "", spec_model: printTarget.spec_model ?? "", order_no: printTarget.order_no ?? "", copies: printCopies }); toast(r.message, "success"); setPrintTarget(null); } catch (err: unknown) { toast(extractErrorMessage(err, "打印失败"), "error"); } finally { setPrinting(false); } }
|
||||
|
||||
// ---- 编辑 ----
|
||||
function openEdit(p: ProductResponse) { setEditTarget(p); setEditOrderNo(p.order_no ?? ""); setEditExternalSerial(p.external_serial ?? ""); }
|
||||
async function handleSaveEdit() { if (!editTarget) return; setEditSaving(true); try { await api.patch(`/products/${editTarget.id}`, { order_no: editOrderNo.trim() || null, external_serial: editExternalSerial.trim() || null }); toast("保存成功", "success"); setEditTarget(null); loadProducts(); } catch (err: any) { toast(err?.response?.data?.detail ?? "保存失败", "error"); } finally { setEditSaving(false); } }
|
||||
async function handleSaveEdit() { if (!editTarget) return; setEditSaving(true); try { await api.patch(`/products/${editTarget.id}`, { order_no: editOrderNo.trim() || null, external_serial: editExternalSerial.trim() || null }); toast("保存成功", "success"); setEditTarget(null); loadProducts(); } catch (err: unknown) { toast(extractErrorMessage(err, "保存失败"), "error"); } finally { setEditSaving(false); } }
|
||||
|
||||
// ---- 删除 ----
|
||||
useEffect(() => {
|
||||
@ -141,7 +142,7 @@ export default function AdminProductsPage() {
|
||||
}, [deleteTarget?.id]); // eslint-disable-line
|
||||
|
||||
function confirmDelete(p: ProductResponse) { setDeleteTarget(p); }
|
||||
async function handleDelete() { if (!deleteTarget) return; setDeleting(true); try { await api.delete(`/products/${deleteTarget.id}`); toast("已删除", "success"); setDeleteTarget(null); loadProducts(); } catch (err: any) { toast(err?.response?.data?.detail ?? "删除失败", "error"); } finally { setDeleting(false); } }
|
||||
async function handleDelete() { if (!deleteTarget) return; setDeleting(true); try { await api.delete(`/products/${deleteTarget.id}`); toast("已删除", "success"); setDeleteTarget(null); loadProducts(); } catch (err: unknown) { toast(extractErrorMessage(err, "删除失败"), "error"); } finally { setDeleting(false); } }
|
||||
|
||||
// ---- 状态收口(入库 / 出库,管理员)----
|
||||
// 🛡️ 双重确认倒计时 + 必须输入产品身份证,防误触
|
||||
@ -164,8 +165,8 @@ export default function AdminProductsPage() {
|
||||
toast(`已收口为「${finalizeStatus}」`, "success");
|
||||
setFinalizeTarget(null);
|
||||
loadProducts();
|
||||
} catch (err: any) {
|
||||
toast(err?.response?.data?.detail ?? "操作失败", "error");
|
||||
} catch (err: unknown) {
|
||||
toast(extractErrorMessage(err, "操作失败"), "error");
|
||||
} finally {
|
||||
setFinalizing(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user