fix: restore mobile report scrolling and published product edits
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

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:
Jesse_Chen
2026-08-18 17:28:22 +08:00
parent 3c9b0bb32b
commit f4a2ba86ae
13 changed files with 398 additions and 8 deletions
@@ -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>