feat: schedule student supervision rules

This commit is contained in:
Codex
2026-06-30 17:40:03 +08:00
parent 0204c934d8
commit c7ad3450fd
21 changed files with 1679 additions and 482 deletions

View File

@@ -9,6 +9,7 @@ import {
loadTenantStudentFollowups,
loadTenantStudentFollowupReport,
loadTenantStudentNotes,
loadTenantStudentSupervisionRules,
loadTenantStudents,
loadTenantTeachers,
loadTenantMembers,
@@ -18,6 +19,7 @@ import {
upsertTenantStudent,
upsertTenantStudentFollowup,
upsertTenantStudentNote,
upsertTenantStudentSupervisionRule,
type BulkOperationResult,
type TenantClassItem,
type TenantStudentFollowupItem,
@@ -27,6 +29,7 @@ import {
type TenantStudentCrmPushInput,
type TenantStudentFollowupReport,
type TenantStudentSupervisionCandidate,
type TenantStudentSupervisionRuleItem,
type TenantTeacherItem,
type TenantMemberItem,
} from '@/services/tenantAdmin';
@@ -139,6 +142,7 @@ export default function TenantStudentsPage() {
const [supervisionResult, setSupervisionResult] = useState<BulkOperationResult | null>(null);
const [followupReport, setFollowupReport] = useState<TenantStudentFollowupReport['item'] | null>(null);
const [supervisionCandidates, setSupervisionCandidates] = useState<TenantStudentSupervisionCandidate[]>([]);
const [supervisionRulesList, setSupervisionRulesList] = useState<TenantStudentSupervisionRuleItem[]>([]);
const [supervisionRules, setSupervisionRules] = useState(defaultSupervisionRules);
const [scoped, setScoped] = useState(false);
const [busy, setBusy] = useState('');
@@ -163,7 +167,8 @@ export default function TenantStudentsPage() {
}),
loadTenantStudentFollowups({ status: 'open', limit: 30 }).catch(() => ({ items: [] })),
loadTenantStudentFollowupReport({ timeRange: '30d', classId: nextClassId || undefined, limit: 8 }).catch(() => ({ item: null })),
]).then(([classPayload, teacherPayload, memberPayload, studentPayload, followupPayload, reportPayload]) => {
loadTenantStudentSupervisionRules({ limit: 20 }).catch(() => ({ items: [] })),
]).then(([classPayload, teacherPayload, memberPayload, studentPayload, followupPayload, reportPayload, supervisionRulePayload]) => {
setClasses(classPayload.items || []);
setTeachers(teacherPayload.items || []);
setCrmAssignees((memberPayload.items || []).filter(item => crmAssignableRoles.includes(String(item.role || ''))));
@@ -171,6 +176,7 @@ export default function TenantStudentsPage() {
setScoped(studentPayload.scoped === true);
setFollowups(followupPayload.items || []);
setFollowupReport(reportPayload.item || null);
setSupervisionRulesList(supervisionRulePayload.items || []);
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '学生数据加载失败'));
}
@@ -391,6 +397,36 @@ export default function TenantStudentsPage() {
}
}
async function saveSupervisionRule() {
setBusy('supervisionRule');
setError('');
try {
const payload = await upsertTenantStudentSupervisionRule({
name: `${activeClass?.name || '全租户'} 学习督导`,
status: 'active',
rules: supervisionRules,
schedule: {
enabled: true,
frequency: 'daily',
hour: 9,
minute: 0,
timezone: 'Asia/Shanghai',
weekdays: [1, 2, 3, 4, 5, 6, 7],
},
classId: selectedClassId || null,
limit: 20,
metadata: { source: 'taro-tenant-admin' },
});
setSupervisionRulesList(prev => [payload.item, ...prev.filter(item => item.id !== payload.item?.id)].filter(Boolean) as TenantStudentSupervisionRuleItem[]);
Taro.showToast({ title: '督导规则已保存', icon: 'success' });
reload();
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : '督导规则保存失败');
} finally {
setBusy('');
}
}
async function saveNote() {
if (!selectedStudent) {
Taro.showToast({ title: '请选择学生', icon: 'none' });
@@ -647,6 +683,7 @@ export default function TenantStudentsPage() {
<View className='admin-actions compact'>
<Button className='admin-button primary' loading={busy === 'supervisionPreview'} onClick={previewSupervision}></Button>
<Button className='admin-button' loading={busy === 'supervisionGenerate'} onClick={generateSupervision}></Button>
<Button className='admin-button' loading={busy === 'supervisionRule'} onClick={saveSupervisionRule}></Button>
{supervisionResult ? <Text className='admin-row-meta'>{resultSummary(supervisionResult)}</Text> : null}
</View>
<View className='admin-list'>
@@ -658,6 +695,14 @@ export default function TenantStudentsPage() {
))}
</View>
{!supervisionCandidates.length ? <Text className='admin-row-meta'></Text> : null}
<View className='admin-list'>
{supervisionRulesList.slice(0, 5).map(item => (
<View className='admin-row' key={item.id}>
<Text className='admin-row-main'>{item.name} · {item.status || 'active'}</Text>
<Text className='admin-row-meta'> {item.className || '全租户'} · {String(item.nextRunAt || '-')} · {String(item.lastResult?.status || '未运行')}</Text>
</View>
))}
</View>
</View>
<View className='admin-section'>

View File

@@ -208,6 +208,39 @@ export interface TenantStudentSupervisionPreview {
};
}
export interface TenantStudentSupervisionSchedule {
enabled?: boolean;
frequency?: 'manual' | 'daily' | 'weekly';
hour?: number;
minute?: number;
timezone?: string;
weekdays?: number[];
}
export interface TenantStudentSupervisionRuleItem {
id: string;
name: string;
status?: string;
rules?: TenantStudentSupervisionRules;
schedule?: TenantStudentSupervisionSchedule;
classId?: string | null;
className?: string | null;
assignedToUserId?: string | null;
assignedToName?: string | null;
limit?: number;
metadata?: Record<string, unknown>;
lastRunAt?: string | null;
nextRunAt?: string | null;
lastResult?: Record<string, unknown>;
createdAt?: string;
updatedAt?: string;
}
export interface TenantStudentSupervisionRulesPayload {
items?: TenantStudentSupervisionRuleItem[];
scoped?: boolean;
}
export interface ImportJobItem {
id: string;
importType?: string;
@@ -1157,6 +1190,32 @@ export async function previewTenantStudentSupervision(query: TenantStudentSuperv
});
}
export async function loadTenantStudentSupervisionRules(query: {
status?: string;
limit?: number;
} = {}) {
return apiRequest<TenantStudentSupervisionRulesPayload>('/api/tenant-admin/students/supervision/rules', {
query: { ...query, limit: query.limit || 50 },
});
}
export async function upsertTenantStudentSupervisionRule(input: {
id?: string;
name: string;
status?: string;
rules?: TenantStudentSupervisionRules;
schedule?: TenantStudentSupervisionSchedule;
classId?: string | null;
assignedToUserId?: string | null;
limit?: number;
metadata?: Record<string, unknown>;
}) {
return apiRequest<{ item?: TenantStudentSupervisionRuleItem }>('/api/tenant-admin/students/supervision/rules', {
method: 'PUT',
body: input,
});
}
export async function generateTenantStudentSupervision(input: {
rules?: TenantStudentSupervisionRules;
classId?: string | null;