diff --git a/app/lib/features/editor/views/editor_page.dart b/app/lib/features/editor/views/editor_page.dart index 95b3ec8..565094b 100644 --- a/app/lib/features/editor/views/editor_page.dart +++ b/app/lib/features/editor/views/editor_page.dart @@ -254,7 +254,7 @@ class EditorPage extends StatelessWidget { } } -class _EditorView extends StatelessWidget { +class _EditorView extends StatefulWidget { final String? journalId; final String? templateId; final String? savedJournalId; @@ -269,6 +269,67 @@ class _EditorView extends StatelessWidget { required this.onSaveComplete, }); + @override + State<_EditorView> createState() => _EditorViewState(); +} + +class _EditorViewState extends State<_EditorView> { + @override + void initState() { + super.initState(); + // 当 journalId 非空时,从 Isar 加载已有日记数据 + if (widget.journalId != null) { + _loadExistingJournal(widget.journalId!); + } + } + + /// 从 Isar 加载已有日记的笔画、元素、标签、心情、标题 + Future _loadExistingJournal(String id) async { + try { + // 加载日记元数据 + final entry = await widget.repo.getJournal(id); + if (entry == null || !mounted) return; + + final bloc = context.read(); + + // 加载标题和心情 + bloc.add(TitleChanged(entry.title)); + bloc.add(MoodChanged(entry.mood)); + + // 加载标签 + if (entry.tags.isNotEmpty) { + bloc.add(TagsLoaded(entry.tags)); + } + + // 加载元素(含笔画) + final elements = await widget.repo.getElements(id); + if (!mounted) return; + + for (final element in elements) { + if (element.elementType == ElementType.handwritingRef) { + // 从 handwriting_ref 元素中恢复笔画 + final strokesData = element.content['strokes']; + if (strokesData is List) { + final strokes = strokesData + .map((s) => Stroke.fromJson(s as Map)) + .toList(); + bloc.add(StrokesLoaded(strokes)); + } + } + } + + // 加载非笔画元素(贴纸/文字/图片) + final nonStrokeElements = elements + .where((e) => e.elementType != ElementType.handwritingRef) + .toList(); + if (nonStrokeElements.isNotEmpty) { + bloc.add(ElementsLoaded(nonStrokeElements)); + } + } catch (e) { + debugPrint('加载日记数据失败: $e'); + } + } + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; @@ -288,7 +349,7 @@ class _EditorView extends StatelessWidget { Expanded( child: BlocBuilder( builder: (context, state) { - return _EditorStack(state: state, journalId: journalId); + return _EditorStack(state: state, journalId: widget.journalId); }, ), ), @@ -404,7 +465,7 @@ class _EditorView extends StatelessWidget { /// 保存处理 void _handleSave(BuildContext context, EditorState state) { - onSaveComplete(); + widget.onSaveComplete(); } /// 格式化日期显示 diff --git a/apps/web/src/api/copilot.ts b/apps/web/src/api/copilot.ts deleted file mode 100644 index 535e48e..0000000 --- a/apps/web/src/api/copilot.ts +++ /dev/null @@ -1,66 +0,0 @@ -import client from './client'; -import type { PaginatedResponse } from './types'; - -// --- Types --- - -export type InsightType = 'risk_score' | 'anomaly' | 'follow_up_hint' | 'consult_hint'; -export type InsightSource = 'rule' | 'llm' | 'hybrid'; -export type RiskLevel = 'low' | 'medium' | 'high' | 'critical'; - -export interface MatchedRule { - rule_id: string; - name: string; - score: number; - severity: string; - suggestion?: string; -} - -export interface RiskScore { - score: number; - level: RiskLevel; - matched_rules: MatchedRule[]; -} - -export interface CopilotInsight { - id: string; - patient_id: string; - insight_type: InsightType; - source: InsightSource; - severity: 'info' | 'warning' | 'critical'; - title: string; - content: Record; - rule_matches?: MatchedRule[]; - llm_supplement?: string; - created_at: string; -} - -// --- API Functions --- - -export async function getPatientRisk(patientId: string) { - const { data } = await client.get<{ success: boolean; data: RiskScore }>(`/copilot/patients/${patientId}/risk`); - return data.data; -} - -export async function listInsights(params: { - patient_id?: string; - insight_type?: string; - severity?: string; - page?: number; - page_size?: number; -}) { - const { data } = await client.get<{ success: boolean; data: PaginatedResponse }>('/copilot/insights', { params }); - return data.data; -} - -export function dismissInsight(id: string) { - return client.post(`/copilot/insights/${id}/dismiss`); -} - -export function listAlerts(params?: { severity?: string; page?: number; page_size?: number }) { - return listInsights({ insight_type: 'anomaly', ...params }); -} - -export async function listRules(params?: { page?: number; page_size?: number }) { - const { data } = await client.get<{ success: boolean; data: PaginatedResponse> }>('/copilot/rules', { params }); - return data.data; -} diff --git a/apps/web/src/api/diary/classes.ts b/apps/web/src/api/diary/classes.ts index d70bda4..aa11cbb 100644 --- a/apps/web/src/api/diary/classes.ts +++ b/apps/web/src/api/diary/classes.ts @@ -1,20 +1,23 @@ import client from '../client'; -import type { SchoolClass, CreateClassReq, ClassMember, PaginatedResponse } from './types'; +import type { SchoolClass, CreateClassReq, UpdateClassReq, ClassMember, PaginatedResponse } from './types'; export const classApi = { - /** 班级列表 — 后端返回纯数组,前端转换为 PaginatedResponse 格式 */ + /** 班级列表 — 当前用户加入的班级 (GET /diary/classes → my_classes) */ list: (params?: { page?: number; page_size?: number }) => client.get<{ success: boolean; data: SchoolClass[] }>('/diary/classes', { params }) .then((r) => { const raw = r.data.data; - // 后端返回纯数组,包装为分页格式 if (Array.isArray(raw)) { return { data: raw, total: raw.length, page: params?.page ?? 1, page_size: params?.page_size ?? 20 } as PaginatedResponse; } - // 兼容:如果后端已升级为分页格式 return raw as unknown as PaginatedResponse; }), + /** 所有班级 — 管理端用,需要 diary.class.manage 权限 (GET /diary/classes/all) */ + listAll: () => + client.get<{ success: boolean; data: SchoolClass[] }>('/diary/classes/all') + .then((r) => r.data.data), + myClasses: () => client.get<{ success: boolean; data: SchoolClass[] }>('/diary/classes/my') .then((r) => r.data.data), @@ -26,6 +29,18 @@ export const classApi = { create: (data: CreateClassReq) => client.post('/diary/classes', data).then((r) => r.data.data), + /** 更新班级信息 (PUT /diary/classes/:id) */ + update: (id: string, data: UpdateClassReq) => + client.put(`/diary/classes/${id}`, data).then((r) => r.data.data), + + /** 停用班级 (PATCH /diary/classes/:id/deactivate) */ + deactivate: (id: string) => + client.patch(`/diary/classes/${id}/deactivate`).then((r) => r.data.data), + + /** 重置班级码 (POST /diary/classes/:id/reset-code) */ + resetCode: (id: string) => + client.post(`/diary/classes/${id}/reset-code`).then((r) => r.data.data), + listMembers: (classId: string) => client.get<{ success: boolean; data: ClassMember[] }>(`/diary/classes/${classId}/members`) .then((r) => r.data.data), diff --git a/apps/web/src/api/diary/types.ts b/apps/web/src/api/diary/types.ts index 5c19412..2d38741 100644 --- a/apps/web/src/api/diary/types.ts +++ b/apps/web/src/api/diary/types.ts @@ -50,6 +50,17 @@ export interface CreateClassReq { school_name?: string; } +export interface UpdateClassReq { + name?: string; + school_name?: string; + version: number; +} + +export interface ResetClassCodeResp { + class_id: string; + new_class_code: string; +} + export interface ClassMember { user_id: string; role: string; diff --git a/apps/web/src/pages/diary/ClassList.tsx b/apps/web/src/pages/diary/ClassList.tsx index 62e3284..2f22c47 100644 --- a/apps/web/src/pages/diary/ClassList.tsx +++ b/apps/web/src/pages/diary/ClassList.tsx @@ -11,6 +11,7 @@ import { Typography, message, Tooltip, + Popconfirm, } from 'antd'; import { PlusOutlined, @@ -18,6 +19,9 @@ import { TeamOutlined, UserOutlined, ReloadOutlined, + StopOutlined, + SyncOutlined, + EditOutlined, } from '@ant-design/icons'; import { classApi } from '../../api/diary/classes'; import type { SchoolClass, ClassMember } from '../../api/diary/types'; @@ -39,8 +43,12 @@ export default function ClassList() { loading, refresh, } = usePaginatedData(async (p: number, pageSize: number) => { - const result = await classApi.list({ page: p, page_size: pageSize }); - return { data: result.data, total: result.total }; + // 管理端使用 listAll 获取所有班级 + const result = await classApi.listAll(); + // 前端分页 + const start = (p - 1) * pageSize; + const sliced = result.slice(start, start + pageSize); + return { data: sliced, total: result.length }; }, 20); // --- Create/Edit drawer --- @@ -49,8 +57,12 @@ 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 () => { - // Class update API not yet available; refresh list silently + onUpdate: async (record, values) => { + await classApi.update(record.id, { + name: values.name as string, + school_name: values.school_name as string | undefined, + version: record.version, + }); }, onSuccess: refresh, }); @@ -84,6 +96,28 @@ export default function ClassList() { ); }, []); + // --- Deactivate class --- + const handleDeactivate = useCallback(async (record: SchoolClass) => { + try { + await classApi.deactivate(record.id); + message.success(`班级「${record.name}」已停用`); + refresh(); + } catch { + message.error('停用班级失败'); + } + }, [refresh]); + + // --- Reset class code --- + const handleResetCode = useCallback(async (record: SchoolClass) => { + try { + const result = await classApi.resetCode(record.id); + message.success(`班级码已重置为 ${result.new_class_code}`); + refresh(); + } catch { + message.error('重置班级码失败'); + } + }, [refresh]); + // --- Table columns --- const columns = [ { @@ -186,7 +220,7 @@ export default function ClassList() { { title: '操作', key: 'actions', - width: 120, + width: 200, render: (_: unknown, record: SchoolClass) => ( @@ -198,6 +232,50 @@ export default function ClassList() { style={{ color: isDark ? '#94a3b8' : '#475569' }} /> + +