Phase 1 — 品牌替换:
- BRAND_DEFAULTS 回退值改为暖记品牌 (themes.ts)
- 登录页/侧边栏/底部回退文字 → 暖记 (Login, MainLayout)
- index.html title/meta/favicon → 暖记
- localStorage key → nuanji-theme, 默认主题 → warm
- 4 套主题色适配暖记设计系统 (珊瑚 #E07A5F / 蓝 / 深色 / 鼠尾草绿)
- 品牌信息通过系统设置配置,不硬编码
Phase 2 — 清理 HMS 模块:
- 删除 health/ai 页面 (~55+2)、API (~30+9)、组件、stores、hooks
- 重写 Home.tsx 为暖记 Dashboard
- 重写 NotificationPanel/MediaPicker 移除 health 依赖
- 清理 routeConfig 移除所有 health/ai 路由权限
Phase 3 — 暖记管理页面:
- API 层: api/diary/{types,journals,classes,topics,comments,stickers}.ts
- 班级管理: 班级列表+创建+成员查看+班级码复制 (ClassList)
- 日记审核: 日记列表+筛选+详情+老师点评 (JournalList)
- 主题管理: 班级选择+主题卡片+创建+过期标记 (TopicList)
- 贴纸管理: 贴纸包卡片+贴纸详情网格 (StickerPackList)
- 路由注册: /diary/classes, /diary/journals, /diary/topics, /diary/stickers
验证: tsc 0 error, vite build ✓, vitest 226/226 pass
366 lines
9.8 KiB
TypeScript
366 lines
9.8 KiB
TypeScript
import { useState, useCallback } from 'react';
|
||
import {
|
||
Table,
|
||
Button,
|
||
Space,
|
||
Form,
|
||
Input,
|
||
Tag,
|
||
Drawer,
|
||
Modal,
|
||
Badge,
|
||
Typography,
|
||
message,
|
||
Tooltip,
|
||
} from 'antd';
|
||
import {
|
||
PlusOutlined,
|
||
CopyOutlined,
|
||
TeamOutlined,
|
||
UserOutlined,
|
||
ReloadOutlined,
|
||
} from '@ant-design/icons';
|
||
import { classApi } from '../../api/diary/classes';
|
||
import type { SchoolClass, ClassMember } from '../../api/diary/types';
|
||
import { PageContainer } from '../../components/PageContainer';
|
||
import { DrawerForm } from '../../components/DrawerForm';
|
||
import { useCrudDrawer } from '../../hooks/useCrudDrawer';
|
||
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
||
import { useApiRequest } from '../../hooks/useApiRequest';
|
||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||
|
||
const { Text } = Typography;
|
||
|
||
export default function ClassList() {
|
||
const isDark = useThemeMode();
|
||
const { execute } = useApiRequest();
|
||
|
||
const {
|
||
data: classes,
|
||
total,
|
||
page,
|
||
loading,
|
||
refresh,
|
||
} = usePaginatedData<SchoolClass>(async (p, pageSize) => {
|
||
const result = await classApi.list({ page: p, page_size: pageSize });
|
||
return { data: result.data, total: result.total };
|
||
}, 20);
|
||
|
||
// --- Create/Edit drawer ---
|
||
const classDrawer = useCrudDrawer<SchoolClass>({
|
||
getId: (r) => r.id,
|
||
onCreate: async (values) => {
|
||
await classApi.create({ name: values.name as string, school_name: values.school_name as string | undefined });
|
||
},
|
||
onUpdate: async () => {
|
||
// Class update API not yet available; refresh list silently
|
||
},
|
||
onSuccess: refresh,
|
||
});
|
||
|
||
// --- Member drawer ---
|
||
const [memberDrawerOpen, setMemberDrawerOpen] = useState(false);
|
||
const [memberClass, setMemberClass] = useState<SchoolClass | null>(null);
|
||
const [members, setMembers] = useState<ClassMember[]>([]);
|
||
const [membersLoading, setMembersLoading] = useState(false);
|
||
|
||
const openMemberDrawer = useCallback(async (cls: SchoolClass) => {
|
||
setMemberClass(cls);
|
||
setMemberDrawerOpen(true);
|
||
setMembersLoading(true);
|
||
try {
|
||
const result = await classApi.listMembers(cls.id);
|
||
setMembers(result);
|
||
} catch {
|
||
message.error('加载班级成员失败');
|
||
setMembers([]);
|
||
} finally {
|
||
setMembersLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
// --- Copy class code ---
|
||
const handleCopyCode = useCallback((code: string) => {
|
||
navigator.clipboard.writeText(code).then(
|
||
() => message.success('班级码已复制'),
|
||
() => message.error('复制失败,请手动复制'),
|
||
);
|
||
}, []);
|
||
|
||
// --- Table columns ---
|
||
const columns = [
|
||
{
|
||
title: '班级名称',
|
||
dataIndex: 'name',
|
||
key: 'name',
|
||
render: (name: string, record: SchoolClass) => (
|
||
<Button
|
||
type="link"
|
||
style={{ padding: 0, height: 'auto', fontWeight: 500, fontSize: 14 }}
|
||
onClick={() => openMemberDrawer(record)}
|
||
>
|
||
{name}
|
||
</Button>
|
||
),
|
||
},
|
||
{
|
||
title: '学校',
|
||
dataIndex: 'school_name',
|
||
key: 'school_name',
|
||
render: (v?: string) => v || <Text type="secondary">-</Text>,
|
||
},
|
||
{
|
||
title: '班级码',
|
||
dataIndex: 'class_code',
|
||
key: 'class_code',
|
||
width: 180,
|
||
render: (code: string) => (
|
||
<Space size={6}>
|
||
<Text
|
||
code
|
||
style={{
|
||
fontFamily: 'JetBrains Mono, Consolas, monospace',
|
||
fontSize: 14,
|
||
letterSpacing: 1,
|
||
}}
|
||
>
|
||
{code}
|
||
</Text>
|
||
<Tooltip title="复制班级码">
|
||
<Button
|
||
type="text"
|
||
size="small"
|
||
icon={<CopyOutlined />}
|
||
onClick={() => handleCopyCode(code)}
|
||
style={{ color: isDark ? '#94a3b8' : '#475569' }}
|
||
/>
|
||
</Tooltip>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '成员数',
|
||
dataIndex: 'member_count',
|
||
key: 'member_count',
|
||
width: 100,
|
||
align: 'center' as const,
|
||
render: (count: number) => (
|
||
<Badge
|
||
count={count}
|
||
showZero
|
||
style={{
|
||
backgroundColor: isDark ? '#1f1f1f' : '#f0f0f0',
|
||
color: isDark ? '#e0e0e0' : '#333',
|
||
fontWeight: 500,
|
||
}}
|
||
overflowCount={9999}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'is_active',
|
||
key: 'is_active',
|
||
width: 100,
|
||
align: 'center' as const,
|
||
render: (isActive: boolean) => (
|
||
<Tag
|
||
color={isActive ? 'success' : 'error'}
|
||
style={{ fontWeight: 500, border: 'none' }}
|
||
>
|
||
{isActive ? '活跃' : '已停用'}
|
||
</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: '教师',
|
||
dataIndex: 'teacher_id',
|
||
key: 'teacher_id',
|
||
render: (teacherId: string) =>
|
||
teacherId ? (
|
||
<Space size={4}>
|
||
<UserOutlined style={{ color: isDark ? '#94a3b8' : '#475569' }} />
|
||
<Text type="secondary">{teacherId}</Text>
|
||
</Space>
|
||
) : (
|
||
<Text type="secondary">-</Text>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 120,
|
||
render: (_: unknown, record: SchoolClass) => (
|
||
<Space size={4}>
|
||
<Tooltip title="查看成员">
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<TeamOutlined />}
|
||
onClick={() => openMemberDrawer(record)}
|
||
style={{ color: isDark ? '#94a3b8' : '#475569' }}
|
||
/>
|
||
</Tooltip>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
// --- Member table columns ---
|
||
const memberColumns = [
|
||
{
|
||
title: '昵称',
|
||
dataIndex: 'nickname',
|
||
key: 'nickname',
|
||
render: (v?: string) => v || <Text type="secondary">未设置</Text>,
|
||
},
|
||
{
|
||
title: '角色',
|
||
dataIndex: 'role',
|
||
key: 'role',
|
||
render: (role: string) => {
|
||
const roleMap: Record<string, { label: string; color: string }> = {
|
||
teacher: { label: '教师', color: 'blue' },
|
||
student: { label: '学生', color: 'green' },
|
||
parent: { label: '家长', color: 'orange' },
|
||
};
|
||
const info = roleMap[role] || { label: role, color: 'default' };
|
||
return <Tag color={info.color}>{info.label}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: '加入时间',
|
||
dataIndex: 'joined_at',
|
||
key: 'joined_at',
|
||
render: (v: string) => (v ? new Date(v).toLocaleDateString('zh-CN') : '-'),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<PageContainer
|
||
title="班级管理"
|
||
subtitle="管理班级信息、班级码和成员"
|
||
actions={
|
||
<Space>
|
||
<Button icon={<ReloadOutlined />} onClick={() => refresh()}>
|
||
刷新
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
onClick={() => classDrawer.openCreate()}
|
||
>
|
||
创建班级
|
||
</Button>
|
||
</Space>
|
||
}
|
||
>
|
||
<Table
|
||
columns={columns}
|
||
dataSource={classes}
|
||
rowKey="id"
|
||
loading={loading}
|
||
pagination={{
|
||
current: page,
|
||
total,
|
||
pageSize: 20,
|
||
onChange: (p) => refresh(p),
|
||
showTotal: (t) => `共 ${t} 条记录`,
|
||
}}
|
||
onRow={(record) => ({
|
||
onClick: () => openMemberDrawer(record),
|
||
style: { cursor: 'pointer' },
|
||
})}
|
||
/>
|
||
|
||
{/* Create class drawer */}
|
||
<DrawerForm
|
||
title={classDrawer.editingRecord ? '编辑班级' : '创建班级'}
|
||
open={classDrawer.open}
|
||
onClose={classDrawer.close}
|
||
onSubmit={classDrawer.handleSubmit}
|
||
initialValues={classDrawer.initialValues}
|
||
loading={classDrawer.loading}
|
||
width={480}
|
||
columns={1}
|
||
>
|
||
<Form.Item
|
||
name="name"
|
||
label="班级名称"
|
||
rules={[{ required: true, message: '请输入班级名称' }]}
|
||
>
|
||
<Input placeholder="例如:三年级二班" maxLength={50} />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="school_name"
|
||
label="学校名称"
|
||
>
|
||
<Input placeholder="例如:阳光小学" maxLength={100} />
|
||
</Form.Item>
|
||
</DrawerForm>
|
||
|
||
{/* Member drawer */}
|
||
<Drawer
|
||
title={
|
||
<Space>
|
||
<TeamOutlined />
|
||
<span>{memberClass ? `${memberClass.name} - 班级成员` : '班级成员'}</span>
|
||
{memberClass && (
|
||
<Tag
|
||
color={memberClass.is_active ? 'success' : 'error'}
|
||
style={{ marginLeft: 8 }}
|
||
>
|
||
{memberClass.is_active ? '活跃' : '已停用'}
|
||
</Tag>
|
||
)}
|
||
</Space>
|
||
}
|
||
open={memberDrawerOpen}
|
||
onClose={() => {
|
||
setMemberDrawerOpen(false);
|
||
setMemberClass(null);
|
||
setMembers([]);
|
||
}}
|
||
width={600}
|
||
styles={{
|
||
body: { background: isDark ? '#141414' : undefined, padding: 0 },
|
||
}}
|
||
extra={
|
||
memberClass ? (
|
||
<Space>
|
||
<Text type="secondary">班级码:</Text>
|
||
<Text
|
||
code
|
||
style={{
|
||
fontFamily: 'JetBrains Mono, Consolas, monospace',
|
||
fontSize: 14,
|
||
letterSpacing: 1,
|
||
}}
|
||
>
|
||
{memberClass.class_code}
|
||
</Text>
|
||
<Button
|
||
size="small"
|
||
icon={<CopyOutlined />}
|
||
onClick={() => handleCopyCode(memberClass.class_code)}
|
||
>
|
||
复制
|
||
</Button>
|
||
</Space>
|
||
) : null
|
||
}
|
||
>
|
||
<Table
|
||
columns={memberColumns}
|
||
dataSource={members}
|
||
rowKey="user_id"
|
||
loading={membersLoading}
|
||
pagination={false}
|
||
size="middle"
|
||
locale={{ emptyText: '暂无成员' }}
|
||
/>
|
||
</Drawer>
|
||
</PageContainer>
|
||
);
|
||
}
|