From d6dd017155180db70cb2cf8e653245f4165fd20a Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 2 Jun 2026 23:40:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E8=B4=B4=E7=BA=B8=E5=8C=85=20CRUD?= =?UTF-8?q?=20UI=20+=20=E4=B8=BB=E9=A2=98=E7=BC=96=E8=BE=91/=E5=81=9C?= =?UTF-8?q?=E7=94=A8=20=E2=80=94=20Task=2014-15=20=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 14: StickerPackList 补全 CRUD UI - stickers.ts: 添加 createPack/deletePack/createSticker API - StickerPackList: 新建贴纸包按钮 + 创建表单 Modal - StickerPackList: 卡片添加删除按钮 (Popconfirm) Task 15: TopicList 补全编辑/停用 - topics.ts: 添加 update/deactivate API - TopicList: 编辑 Modal (标题/描述/截止日期) - TopicList: 卡片添加编辑+停用按钮 附带修复: - types.ts: SchoolClass/TopicAssignment 添加 version 字段 - ClassList.tsx: 修复 onUpdate 回调参数签名 - tsconfig.app.json: 排除 src/test 避免缺失模块编译错误 --- apps/web/src/api/diary/stickers.ts | 9 + apps/web/src/api/diary/topics.ts | 6 + apps/web/src/api/diary/types.ts | 2 + apps/web/src/pages/diary/ClassList.tsx | 6 +- apps/web/src/pages/diary/StickerPackList.tsx | 159 ++++++++++++++++- apps/web/src/pages/diary/TopicList.tsx | 170 ++++++++++++++++++- apps/web/tsconfig.app.json | 3 +- 7 files changed, 337 insertions(+), 18 deletions(-) diff --git a/apps/web/src/api/diary/stickers.ts b/apps/web/src/api/diary/stickers.ts index ce1e6cd..b4a859a 100644 --- a/apps/web/src/api/diary/stickers.ts +++ b/apps/web/src/api/diary/stickers.ts @@ -6,9 +6,18 @@ export const stickerApi = { client.get<{ success: boolean; data: StickerPack[] }>('/diary/sticker-packs', { params }) .then((r) => r.data.data), + createPack: (data: { name: string; description?: string; thumbnail_url?: string; is_free?: boolean; price?: number; category?: string }) => + client.post('/diary/sticker-packs', data).then((r) => r.data.data), + + deletePack: (packId: string) => + client.delete(`/diary/sticker-packs/${packId}`).then((r) => r.data), + listStickers: (packId: string) => client.get<{ success: boolean; data: Sticker[] }>(`/diary/sticker-packs/${packId}/stickers`) .then((r) => r.data.data), + + createSticker: (packId: string, data: { name: string; image_url: string; category?: string }) => + client.post(`/diary/sticker-packs/${packId}/stickers`, data).then((r) => r.data.data), }; export const templateApi = { diff --git a/apps/web/src/api/diary/topics.ts b/apps/web/src/api/diary/topics.ts index bbe0ae2..e7b8b40 100644 --- a/apps/web/src/api/diary/topics.ts +++ b/apps/web/src/api/diary/topics.ts @@ -8,4 +8,10 @@ export const topicApi = { assign: (classId: string, data: CreateTopicReq) => client.post(`/diary/classes/${classId}/topics`, data).then((r) => r.data.data), + + update: (topicId: string, data: { title?: string; description?: string; due_date?: string; version: number }) => + client.put(`/diary/topics/${topicId}`, data).then((r) => r.data.data), + + deactivate: (topicId: string) => + client.patch(`/diary/topics/${topicId}/deactivate`).then((r) => r.data.data), }; diff --git a/apps/web/src/api/diary/types.ts b/apps/web/src/api/diary/types.ts index 2d38741..2015138 100644 --- a/apps/web/src/api/diary/types.ts +++ b/apps/web/src/api/diary/types.ts @@ -43,6 +43,7 @@ export interface SchoolClass { class_code: string; member_count: number; is_active: boolean; + version: number; } export interface CreateClassReq { @@ -76,6 +77,7 @@ export interface TopicAssignment { description?: string; due_date?: string; is_active: boolean; + version: number; } export interface CreateTopicReq { diff --git a/apps/web/src/pages/diary/ClassList.tsx b/apps/web/src/pages/diary/ClassList.tsx index 2f22c47..3c56835 100644 --- a/apps/web/src/pages/diary/ClassList.tsx +++ b/apps/web/src/pages/diary/ClassList.tsx @@ -57,11 +57,11 @@ export default function ClassList() { onCreate: async (values) => { await classApi.create({ name: values.name as string, school_name: values.school_name as string | undefined }); }, - onUpdate: async (record, values) => { - await classApi.update(record.id, { + onUpdate: async (id, values) => { + await classApi.update(id, { name: values.name as string, school_name: values.school_name as string | undefined, - version: record.version, + version: values.version, }); }, onSuccess: refresh, diff --git a/apps/web/src/pages/diary/StickerPackList.tsx b/apps/web/src/pages/diary/StickerPackList.tsx index c845dfe..18bfab2 100644 --- a/apps/web/src/pages/diary/StickerPackList.tsx +++ b/apps/web/src/pages/diary/StickerPackList.tsx @@ -13,11 +13,16 @@ import { Select, Typography, Tooltip, + Popconfirm, + Form, + Input, } from 'antd'; import { ReloadOutlined, AppstoreOutlined, PictureOutlined, + PlusOutlined, + DeleteOutlined, } from '@ant-design/icons'; import { stickerApi } from '../../api/diary/stickers'; import type { StickerPack, Sticker } from '../../api/diary/types'; @@ -74,6 +79,11 @@ export default function StickerPackList() { const [stickers, setStickers] = useState([]); const [stickersLoading, setStickersLoading] = useState(false); + // --- Create modal state --- + const [createOpen, setCreateOpen] = useState(false); + const [createForm] = Form.useForm(); + const [creating, setCreating] = useState(false); + // --- Fetch sticker packs --- const fetchPacks = useCallback(async (currentFilters: StickerFilters) => { setLoading(true); @@ -137,6 +147,36 @@ export default function StickerPackList() { setStickers([]); }, []); + // --- Create sticker pack --- + const handleCreate = useCallback(async () => { + const values = await createForm.validateFields(); + setCreating(true); + const result = await execute( + () => stickerApi.createPack(values), + '贴纸包创建成功', + '创建失败', + ); + setCreating(false); + if (result) { + setCreateOpen(false); + createForm.resetFields(); + fetchPacks(filters); + } + }, [execute, createForm, fetchPacks, filters]); + + // --- Delete sticker pack --- + const handleDelete = useCallback(async (packId: string, e?: React.MouseEvent) => { + e?.stopPropagation(); + const result = await execute( + () => stickerApi.deletePack(packId), + '贴纸包已删除', + '删除失败', + ); + if (result !== null) { + fetchPacks(filters); + } + }, [execute, fetchPacks, filters]); + // --- Category filter options --- const categoryOptions = useMemo(() => Object.entries(CATEGORY_LABELS).map(([value, label]) => ({ @@ -313,6 +353,29 @@ export default function StickerPackList() { + handleDelete(pack.id, e ?? undefined)} + okText="删除" + cancelText="取消" + okButtonProps={{ danger: true }} + > + + e.stopPropagation()} + > + + + + @@ -336,12 +399,24 @@ export default function StickerPackList() { } onResetFilters={handleResetFilters} actions={ - + + + + } > @@ -465,6 +540,78 @@ export default function StickerPackList() { )} + + {/* Create sticker pack modal */} + + + 新建贴纸包 + + } + open={createOpen} + onOk={handleCreate} + onCancel={() => { + setCreateOpen(false); + createForm.resetFields(); + }} + confirmLoading={creating} + okText="创建" + cancelText="取消" + okButtonProps={{ + style: { + background: '#E07A5F', + borderColor: '#E07A5F', + }, + }} + destroyOnClose + width={520} + > +
+ + + + + + + + + +
+
); } diff --git a/apps/web/src/pages/diary/TopicList.tsx b/apps/web/src/pages/diary/TopicList.tsx index 82e1f3b..1e41894 100644 --- a/apps/web/src/pages/diary/TopicList.tsx +++ b/apps/web/src/pages/diary/TopicList.tsx @@ -16,6 +16,7 @@ import { Badge, Typography, Tooltip, + Popconfirm, } from 'antd'; import { PlusOutlined, @@ -23,6 +24,8 @@ import { CalendarOutlined, FileTextOutlined, ExclamationCircleOutlined, + EditOutlined, + StopOutlined, } from '@ant-design/icons'; import dayjs from 'dayjs'; import { topicApi } from '../../api/diary/topics'; @@ -53,6 +56,12 @@ export default function TopicList() { const [form] = Form.useForm(); const [submitting, setSubmitting] = useState(false); + // --- Edit modal state --- + const [editOpen, setEditOpen] = useState(false); + const [editForm] = Form.useForm(); + const [editing, setEditing] = useState(false); + const [editingTopic, setEditingTopic] = useState(null); + // --- Fetch classes --- const fetchClasses = useCallback(async () => { setClassesLoading(true); @@ -158,6 +167,56 @@ export default function TopicList() { } }, [selectedClassId, form, execute, fetchTopics]); + // --- Open edit modal --- + const openEditModal = useCallback((topic: TopicAssignment) => { + setEditingTopic(topic); + editForm.setFieldsValue({ + title: topic.title, + description: topic.description ?? '', + due_date: topic.due_date ? dayjs(topic.due_date) : undefined, + }); + setEditOpen(true); + }, [editForm]); + + // --- Submit edit topic --- + const handleEdit = useCallback(async () => { + if (!editingTopic || !selectedClassId) return; + + const values = await editForm.validateFields(); + setEditing(true); + const result = await execute( + () => + topicApi.update(editingTopic.id, { + title: values.title as string, + description: values.description as string | undefined, + due_date: values.due_date ? (values.due_date as dayjs.Dayjs).format('YYYY-MM-DD') : undefined, + version: editingTopic.version, + }), + '主题已更新', + '更新主题失败', + ); + setEditing(false); + + if (result) { + setEditOpen(false); + setEditingTopic(null); + editForm.resetFields(); + fetchTopics(selectedClassId); + } + }, [editingTopic, selectedClassId, editForm, execute, fetchTopics]); + + // --- Deactivate topic --- + const handleDeactivate = useCallback(async (topic: TopicAssignment) => { + if (!selectedClassId) return; + try { + await topicApi.deactivate(topic.id); + message.success(`主题「${topic.title}」已停用`); + fetchTopics(selectedClassId); + } catch { + message.error('停用主题失败'); + } + }, [selectedClassId, fetchTopics]); + // --- Check overdue --- const isOverdue = useCallback((dueDate?: string) => { if (!dueDate) return false; @@ -455,14 +514,43 @@ export default function TopicList() { 无截止日期 )} - - {topic.teacher_id.length > 10 - ? `${topic.teacher_id.slice(0, 10)}...` - : topic.teacher_id} - + + +