forked from wangziqi/gongxue-base
feat: schedule student supervision rules
This commit is contained in:
@@ -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'>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user