feat(health): 积分规则/商品 update/delete + 标签更新端点
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 积分规则: 添加 update/delete service + handler + 路由
- 兑换商品: 添加 update/delete service + handler + 路由
- 文章标签: 添加 update service + handler + 路由
- Web 管理端: 规则/商品列表页支持编辑/删除/启用切换
- Web 管理端: 标签管理页支持编辑、删除传 version
This commit is contained in:
iven
2026-04-26 14:07:21 +08:00
parent f0076aa240
commit 55ec57b2c0
13 changed files with 504 additions and 71 deletions

View File

@@ -252,11 +252,19 @@ export const articleTagApi = {
return data.data;
},
delete: async (id: string) => {
update: async (id: string, req: { name: string; version: number }) => {
const { data } = await client.put<{
success: boolean;
data: ArticleTagItem;
}>(`/health/article-tags/${id}`, req);
return data.data;
},
delete: async (id: string, version: number) => {
const { data } = await client.delete<{
success: boolean;
data: null;
}>(`/health/article-tags/${id}`);
}>(`/health/article-tags/${id}`, { data: { version } });
return data.data;
},
};

View File

@@ -168,6 +168,20 @@ export const pointsApi = {
return data.data;
},
updateRule: async (id: string, req: Partial<CreatePointsRuleReq> & { is_active?: boolean; version: number }) => {
const { data } = await client.put<{
success: boolean;
data: PointsRule;
}>(`/health/admin/points/rules/${id}`, { data: req, version: req.version });
return data.data;
},
deleteRule: async (id: string, version: number) => {
await client.delete(`/health/admin/points/rules/${id}`, {
data: { version },
});
},
// Products
listProducts: async (params?: Record<string, unknown>) => {
const { data } = await client.get<{
@@ -185,6 +199,20 @@ export const pointsApi = {
return data.data;
},
updateProduct: async (id: string, req: Partial<CreatePointsProductReq> & { is_active?: boolean; version: number }) => {
const { data } = await client.put<{
success: boolean;
data: PointsProduct;
}>(`/health/admin/points/products/${id}`, { data: req, version: req.version });
return data.data;
},
deleteProduct: async (id: string, version: number) => {
await client.delete(`/health/admin/points/products/${id}`, {
data: { version },
});
},
// Orders
listOrders: async (params?: Record<string, unknown>) => {
const { data } = await client.get<{

View File

@@ -9,7 +9,7 @@ import {
Tag,
message,
} from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
import {
articleTagApi,
type ArticleTagItem,
@@ -22,6 +22,7 @@ export default function ArticleTagManage() {
const [tags, setTags] = useState<ArticleTagItem[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<ArticleTagItem | null>(null);
const [form] = Form.useForm();
const isDark = useThemeMode();
@@ -42,37 +43,50 @@ export default function ArticleTagManage() {
}, [fetchTags]);
const openCreateModal = () => {
setEditing(null);
form.resetFields();
setModalOpen(true);
};
const openEditModal = (record: ArticleTagItem) => {
setEditing(record);
form.setFieldsValue({ name: record.name });
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
setEditing(null);
form.resetFields();
};
const handleCreate = async (values: { name: string; slug?: string; color?: string }) => {
const handleSubmit = async (values: { name: string; slug?: string; color?: string }) => {
try {
const req: CreateTagReq = {
name: values.name,
slug: values.slug,
color: values.color,
};
await articleTagApi.create(req);
message.success('标签创建成功');
if (editing) {
await articleTagApi.update(editing.id, { name: values.name, version: editing.version });
message.success('标签更新成功');
} else {
const req: CreateTagReq = {
name: values.name,
slug: values.slug,
color: values.color,
};
await articleTagApi.create(req);
message.success('标签创建成功');
}
closeModal();
fetchTags();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ||
'创建失败';
(editing ? '更新失败' : '创建失败');
message.error(errorMsg);
}
};
const handleDelete = async (id: string) => {
const handleDelete = async (record: ArticleTagItem) => {
try {
await articleTagApi.delete(id);
await articleTagApi.delete(record.id, record.version);
message.success('标签已删除');
fetchTags();
} catch {
@@ -138,13 +152,16 @@ export default function ArticleTagManage() {
width: 80,
render: (_: unknown, record: ArticleTagItem) => (
<AuthButton code="health.articles.manage">
<Popconfirm
title="确定删除此标签?"
description="删除后关联的文章将移除该标签"
onConfirm={() => handleDelete(record.id)}
>
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
</Popconfirm>
<div style={{ display: 'flex', gap: 4 }}>
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => openEditModal(record)} />
<Popconfirm
title="确定删除此标签?"
description="删除后关联的文章将移除该标签"
onConfirm={() => handleDelete(record)}
>
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
</Popconfirm>
</div>
</AuthButton>
),
},
@@ -183,9 +200,9 @@ export default function ArticleTagManage() {
/>
</div>
{/* 新建标签弹窗 */}
{/* 新建/编辑标签弹窗 */}
<Modal
title="新建标签"
title={editing ? '编辑标签' : '新建标签'}
open={modalOpen}
onCancel={closeModal}
onOk={() => form.submit()}
@@ -193,7 +210,7 @@ export default function ArticleTagManage() {
>
<Form
form={form}
onFinish={handleCreate}
onFinish={handleSubmit}
layout="vertical"
style={{ marginTop: 16 }}
>

View File

@@ -19,6 +19,7 @@ import {
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import {
@@ -113,16 +114,23 @@ export default function PointsProductList() {
sort_order?: number;
}) => {
try {
const req: CreatePointsProductReq = {
name: values.name,
product_type: values.product_type,
points_cost: values.points_cost,
stock: values.stock,
description: values.description,
image_url: values.image_url,
sort_order: values.sort_order,
};
await pointsApi.createProduct(req);
if (editing) {
await pointsApi.updateProduct(editing.id, {
...values,
version: editing.version,
});
} else {
const req: CreatePointsProductReq = {
name: values.name,
product_type: values.product_type,
points_cost: values.points_cost,
stock: values.stock,
description: values.description,
image_url: values.image_url,
sort_order: values.sort_order,
};
await pointsApi.createProduct(req);
}
message.success(editing ? '更新成功' : '创建成功');
setModalOpen(false);
form.resetFields();
@@ -135,16 +143,10 @@ export default function PointsProductList() {
// ---- 切换上下架 ----
const handleToggleActive = async (record: PointsProduct) => {
try {
const req: CreatePointsProductReq = {
name: record.name,
product_type: record.product_type,
points_cost: record.points_cost,
stock: record.stock,
description: record.description ?? undefined,
image_url: record.image_url ?? undefined,
sort_order: record.sort_order,
};
await pointsApi.createProduct(req);
await pointsApi.updateProduct(record.id, {
is_active: !record.is_active,
version: record.version,
});
message.success(record.is_active ? '已下架' : '已上架');
fetchData(page, pageSize);
} catch {
@@ -152,6 +154,24 @@ export default function PointsProductList() {
}
};
// ---- 删除 ----
const handleDelete = (record: PointsProduct) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除商品「${record.name}」吗?`,
okType: 'danger',
onOk: async () => {
try {
await pointsApi.deleteProduct(record.id, record.version);
message.success('删除成功');
fetchData(page, pageSize);
} catch {
message.error('删除失败');
}
},
});
};
// ---- 列定义 ----
const columns = [
{
@@ -230,6 +250,15 @@ export default function PointsProductList() {
unCheckedChildren="下架"
onChange={() => handleToggleActive(record)}
/>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>
</Button>
</Space>
</AuthButton>
),

View File

@@ -19,6 +19,7 @@ import {
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import {
@@ -101,17 +102,24 @@ export default function PointsRuleList() {
streak_30d_bonus?: number;
}) => {
try {
const req: CreatePointsRuleReq = {
event_type: values.event_type,
name: values.name,
description: values.description,
points_value: values.points_value,
daily_cap: values.daily_cap,
streak_7d_bonus: values.streak_7d_bonus,
streak_14d_bonus: values.streak_14d_bonus,
streak_30d_bonus: values.streak_30d_bonus,
};
await pointsApi.createRule(req);
if (editing) {
await pointsApi.updateRule(editing.id, {
...values,
version: editing.version,
});
} else {
const req: CreatePointsRuleReq = {
event_type: values.event_type,
name: values.name,
description: values.description,
points_value: values.points_value,
daily_cap: values.daily_cap,
streak_7d_bonus: values.streak_7d_bonus,
streak_14d_bonus: values.streak_14d_bonus,
streak_30d_bonus: values.streak_30d_bonus,
};
await pointsApi.createRule(req);
}
message.success(editing ? '更新成功' : '创建成功');
setModalOpen(false);
form.resetFields();
@@ -124,19 +132,10 @@ export default function PointsRuleList() {
// ---- 切换启用状态 ----
const handleToggleActive = async (record: PointsRule) => {
try {
// 目前后端没有 toggle 接口,重新创建等同于更新
// 使用 create 接口覆盖同 event_type 的规则
const req: CreatePointsRuleReq = {
event_type: record.event_type,
name: record.name,
description: record.description ?? undefined,
points_value: record.points_value,
daily_cap: record.daily_cap,
streak_7d_bonus: record.streak_7d_bonus,
streak_14d_bonus: record.streak_14d_bonus,
streak_30d_bonus: record.streak_30d_bonus,
};
await pointsApi.createRule(req);
await pointsApi.updateRule(record.id, {
is_active: !record.is_active,
version: record.version,
});
message.success(record.is_active ? '已停用' : '已启用');
fetchData();
} catch {
@@ -144,6 +143,24 @@ export default function PointsRuleList() {
}
};
// ---- 删除 ----
const handleDelete = (record: PointsRule) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除规则「${record.name}」吗?`,
okType: 'danger',
onOk: async () => {
try {
await pointsApi.deleteRule(record.id, record.version);
message.success('删除成功');
fetchData();
} catch {
message.error('删除失败');
}
},
});
};
// ---- 列定义 ----
const columns = [
{
@@ -234,6 +251,15 @@ export default function PointsRuleList() {
unCheckedChildren="停用"
onChange={() => handleToggleActive(record)}
/>
<Button
type="link"
size="small"
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(record)}
>
</Button>
</Space>
</AuthButton>
),