feat(health): 积分规则/商品 update/delete + 标签更新端点
- 积分规则: 添加 update/delete service + handler + 路由 - 兑换商品: 添加 update/delete service + handler + 路由 - 文章标签: 添加 update service + handler + 路由 - Web 管理端: 规则/商品列表页支持编辑/删除/启用切换 - Web 管理端: 标签管理页支持编辑、删除传 version
This commit is contained in:
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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 }}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user