fix: restore mobile report scrolling and published product edits
Report pages now scroll inside the chat shell lock, and admin product save forks a draft or retires a published plan instead of rejecting with a generic constraint error. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,7 +37,11 @@ const publishSchema = z.object({
|
||||
action: z.literal("publish"),
|
||||
id: z.string().uuid(),
|
||||
}).strict();
|
||||
const mutationSchema = z.discriminatedUnion("action", [saveSchema, publishSchema]);
|
||||
const deleteSchema = z.object({
|
||||
action: z.literal("delete"),
|
||||
id: z.string().uuid(),
|
||||
}).strict();
|
||||
const mutationSchema = z.discriminatedUnion("action", [saveSchema, publishSchema, deleteSchema]);
|
||||
|
||||
type ProductRow = {
|
||||
id: string; code: string; version: number; name: string; description: string;
|
||||
@@ -92,6 +96,10 @@ export async function POST(request: Request) {
|
||||
const rows = await queryAdminRows<{ id: string }>("select public.admin_publish_product($1,$2,$3,$4) id", [session.user.id, body.data.id, "admin_console_publish_product", rid]);
|
||||
return NextResponse.json({ data: { id: rows[0]!.id, requestId: rid } });
|
||||
}
|
||||
if (body.data.action === "delete") {
|
||||
const rows = await queryAdminRows<{ id: string }>("select public.admin_delete_product($1,$2,$3,$4) id", [session.user.id, body.data.id, "admin_console_delete_product", rid]);
|
||||
return NextResponse.json({ data: { id: rows[0]!.id, requestId: rid } });
|
||||
}
|
||||
const value = body.data;
|
||||
const rows = await queryAdminRows<{ id: string }>(`
|
||||
select public.admin_save_product_draft($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::jsonb,$15,$16) id
|
||||
|
||||
@@ -1756,7 +1756,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.payment-qr-badge svg { display: block; width: 100%; height: 100%; }
|
||||
|
||||
/* Global report centre — quiet archive, independent from chat sessions. */
|
||||
.report-center-shell { width: 100%; min-height: 100dvh; overflow-x: hidden; padding: 0 clamp(20px, 5vw, 72px) 72px; background: var(--color-canvas-soft); color: var(--color-ink); }
|
||||
.report-center-shell { width: 100%; height: 100%; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; padding: 0 clamp(20px, 5vw, 72px) 72px; background: var(--color-canvas-soft); color: var(--color-ink); }
|
||||
.report-center-topbar { max-width: 1080px; min-height: 64px; display: flex; align-items: center; justify-content: space-between; gap: 20px; margin: 0 auto; border-bottom: 1px solid var(--color-border); color: var(--color-ink-tertiary); font-size: 12px; }
|
||||
.report-center-back { min-height: 44px; display: inline-flex; align-items: center; gap: 7px; color: var(--color-ink-secondary); font-size: 14px; font-weight: 500; text-decoration: none; }
|
||||
.report-center-back:hover { color: var(--color-action); }
|
||||
@@ -1808,11 +1808,13 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
--report-paper: #f8f5ee;
|
||||
--report-rule: #c9c2b7;
|
||||
width: 100%;
|
||||
height: 100dvh;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
touch-action: pan-y;
|
||||
scrollbar-gutter: stable;
|
||||
background: var(--report-paper);
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ export default function ProductManagement() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [publishingId, setPublishingId] = useState<string | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const canWrite = Boolean(identity?.permissions.includes("billing.products.write"));
|
||||
const canPublish = Boolean(identity?.permissions.includes("billing.products.publish"));
|
||||
|
||||
@@ -184,7 +185,7 @@ export default function ProductManagement() {
|
||||
entitlements,
|
||||
}),
|
||||
});
|
||||
message.success("商品草稿已保存");
|
||||
message.success(editing && editing.status !== "draft" ? "已保存为新版本草稿,发布后才会替换在售套餐" : "商品草稿已保存");
|
||||
setOpen(false);
|
||||
form.resetFields();
|
||||
await table.tableQuery.refetch();
|
||||
@@ -211,6 +212,22 @@ export default function ProductManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(product: Product) {
|
||||
setDeletingId(product.id);
|
||||
try {
|
||||
await adminRequestJson("/api/admin/products", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action: "delete", id: product.id }),
|
||||
});
|
||||
message.success(product.status === "draft" ? "草稿已删除" : "商品已下架");
|
||||
await table.tableQuery.refetch();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : product.status === "draft" ? "删除草稿失败" : "下架商品失败");
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<Product> = [
|
||||
{
|
||||
title: "商品",
|
||||
@@ -227,8 +244,10 @@ export default function ProductManagement() {
|
||||
title: "操作",
|
||||
fixed: "right",
|
||||
render: (_, item) => <Space>
|
||||
{canWrite && <Button type="link" onClick={() => openProduct(item)}>编辑草稿</Button>}
|
||||
{canPublish && item.status !== "published" && <Button type="link" loading={publishingId === item.id} onClick={() => void publish(item)}>发布</Button>}
|
||||
{canWrite && <Button type="link" onClick={() => openProduct(item)}>{item.status === "draft" ? "编辑草稿" : "修改"}</Button>}
|
||||
{canPublish && item.status === "draft" && <Button type="link" loading={publishingId === item.id} onClick={() => void publish(item)}>发布</Button>}
|
||||
{canWrite && item.status === "draft" && <Button type="link" loading={deletingId === item.id} onClick={() => void remove(item)}>删除</Button>}
|
||||
{canWrite && item.status === "published" && <Button type="link" loading={deletingId === item.id} onClick={() => void remove(item)}>下架</Button>}
|
||||
</Space>,
|
||||
},
|
||||
];
|
||||
@@ -249,6 +268,7 @@ export default function ProductManagement() {
|
||||
<Table {...table.tableProps} columns={columns} rowKey="id" scroll={{ x: "max-content" }} />
|
||||
</Space>
|
||||
<Modal title={editing ? `编辑 ${editing.name}` : "新建商品"} open={open} width={860} confirmLoading={saving} okText="保存草稿" cancelText="取消" onOk={() => form.submit()} onCancel={() => setOpen(false)} destroyOnHidden>
|
||||
{editing && editing.status !== "draft" ? <Text type="secondary">保存后会生成新版本草稿,不会立刻改动当前在售套餐。要从目录拿掉请用列表里的下架。</Text> : null}
|
||||
<Form<ProductForm> form={form} layout="vertical" onFinish={save} requiredMark="optional">
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}><Form.Item name="code" label="商品代码" rules={[{ required: true }, { pattern: /^[a-z][a-z0-9_]{1,79}$/ }]}><Input disabled={Boolean(editing)} /></Form.Item></Col>
|
||||
|
||||
@@ -31,9 +31,21 @@ export function adminErrorResponse(error: unknown) {
|
||||
if (message.includes("模型供应商密钥未配置") || message.includes("model_provider_key_required")) {
|
||||
return NextResponse.json({ error: "模型供应商密钥未配置" }, { status: 409 });
|
||||
}
|
||||
if (message.includes("product_not_found")) {
|
||||
return NextResponse.json({ error: "商品不存在" }, { status: 404 });
|
||||
}
|
||||
if (message.includes("product_code_immutable")) {
|
||||
return NextResponse.json({ error: "商品代码不可修改" }, { status: 400 });
|
||||
}
|
||||
if (message.includes("product_draft_not_found")) {
|
||||
return NextResponse.json({ error: "只能编辑草稿。已发布商品请下架,或保存为新版本草稿后再发布" }, { status: 400 });
|
||||
}
|
||||
if (isPostgresError(error)) {
|
||||
if (error.code === "42501") return NextResponse.json({ error: "无权执行此操作" }, { status: 403 });
|
||||
if (error.code === "40001") return NextResponse.json({ error: "资源已被其他管理员修改,请刷新后重试" }, { status: 409 });
|
||||
if (error.code === "23503") {
|
||||
return NextResponse.json({ error: "已有订单或订阅引用该商品,不能硬删除,请改为下架" }, { status: 409 });
|
||||
}
|
||||
if (error.code === "22023" || error.code === "23514" || error.code === "23505") {
|
||||
return NextResponse.json({ error: "提交内容不符合业务约束" }, { status: 400 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user