forked from wangziqi/gongxue-base
feat: add student supervision automation
This commit is contained in:
@@ -4,6 +4,7 @@ import { Button, Input, Text, Textarea, View } from '@tarojs/components';
|
||||
import {
|
||||
bulkAssignTenantClassMembers,
|
||||
bulkUpsertTenantStudents,
|
||||
generateTenantStudentSupervision,
|
||||
loadTenantClasses,
|
||||
loadTenantStudentFollowups,
|
||||
loadTenantStudentFollowupReport,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
loadTenantStudents,
|
||||
loadTenantTeachers,
|
||||
loadTenantMembers,
|
||||
previewTenantStudentSupervision,
|
||||
pushTenantStudentsToCrm,
|
||||
updateTenantStudentStatus,
|
||||
upsertTenantStudent,
|
||||
@@ -24,6 +26,7 @@ import {
|
||||
type TenantStudentNoteItem,
|
||||
type TenantStudentCrmPushInput,
|
||||
type TenantStudentFollowupReport,
|
||||
type TenantStudentSupervisionCandidate,
|
||||
type TenantTeacherItem,
|
||||
type TenantMemberItem,
|
||||
} from '@/services/tenantAdmin';
|
||||
@@ -35,6 +38,15 @@ const followupTypes = ['learning', 'service', 'sales', 'renewal', 'risk', 'custo
|
||||
const followupPriorities = ['low', 'normal', 'high', 'urgent'];
|
||||
const followupStatuses = ['open', 'in_progress', 'done', 'cancelled'];
|
||||
const crmAssignableRoles = ['tenant_owner', 'tenant_admin', 'tenant_operator', 'teacher', 'sales', 'agent'];
|
||||
const defaultSupervisionRules = {
|
||||
windowDays: 14,
|
||||
inactivityDays: 7,
|
||||
minAnswers: 10,
|
||||
lowAccuracyThreshold: 0.6,
|
||||
wrongQuestionThreshold: 5,
|
||||
vocabularyDueThreshold: 20,
|
||||
staleSessionDays: 3,
|
||||
};
|
||||
|
||||
function emptyStudentForm(): TenantStudentInput {
|
||||
return {
|
||||
@@ -124,7 +136,10 @@ export default function TenantStudentsPage() {
|
||||
const [bulkResult, setBulkResult] = useState<BulkOperationResult | null>(null);
|
||||
const [assignResult, setAssignResult] = useState<BulkOperationResult | null>(null);
|
||||
const [crmPushResult, setCrmPushResult] = useState<BulkOperationResult | null>(null);
|
||||
const [supervisionResult, setSupervisionResult] = useState<BulkOperationResult | null>(null);
|
||||
const [followupReport, setFollowupReport] = useState<TenantStudentFollowupReport['item'] | null>(null);
|
||||
const [supervisionCandidates, setSupervisionCandidates] = useState<TenantStudentSupervisionCandidate[]>([]);
|
||||
const [supervisionRules, setSupervisionRules] = useState(defaultSupervisionRules);
|
||||
const [scoped, setScoped] = useState(false);
|
||||
const [busy, setBusy] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
@@ -332,6 +347,50 @@ export default function TenantStudentsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function previewSupervision() {
|
||||
setBusy('supervisionPreview');
|
||||
setError('');
|
||||
try {
|
||||
const payload = await previewTenantStudentSupervision({
|
||||
...supervisionRules,
|
||||
classId: selectedClassId || undefined,
|
||||
limit: 20,
|
||||
});
|
||||
setSupervisionCandidates(payload.item?.candidates || []);
|
||||
Taro.showToast({ title: '督导候选已刷新', icon: 'success' });
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '学习督导预览失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSupervision() {
|
||||
if (!supervisionCandidates.length) {
|
||||
Taro.showToast({ title: '请先预览候选', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
setBusy('supervisionGenerate');
|
||||
setError('');
|
||||
try {
|
||||
const result = await generateTenantStudentSupervision({
|
||||
rules: supervisionRules,
|
||||
classId: selectedClassId || null,
|
||||
studentUserIds: supervisionCandidates.map(item => item.studentUserId),
|
||||
batchKey: `taro-supervision-${new Date().toISOString().slice(0, 10)}-${selectedClassId || 'all'}`,
|
||||
limit: supervisionCandidates.length,
|
||||
metadata: { source: 'taro-tenant-admin' },
|
||||
});
|
||||
setSupervisionResult(result);
|
||||
Taro.showToast({ title: '督导任务已生成', icon: result.errorCount ? 'none' : 'success' });
|
||||
reload();
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '学习督导生成失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNote() {
|
||||
if (!selectedStudent) {
|
||||
Taro.showToast({ title: '请选择学生', icon: 'none' });
|
||||
@@ -575,6 +634,32 @@ export default function TenantStudentsPage() {
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>学习督导自动化</Text>
|
||||
<View className='admin-form-grid'>
|
||||
<Input className='admin-input' placeholder='观察天数' value={String(supervisionRules.windowDays)} onInput={event => setSupervisionRules(prev => ({ ...prev, windowDays: Number(event.detail.value || prev.windowDays) }))} />
|
||||
<Input className='admin-input' placeholder='未学习天数' value={String(supervisionRules.inactivityDays)} onInput={event => setSupervisionRules(prev => ({ ...prev, inactivityDays: Number(event.detail.value || prev.inactivityDays) }))} />
|
||||
<Input className='admin-input' placeholder='最低答题数' value={String(supervisionRules.minAnswers)} onInput={event => setSupervisionRules(prev => ({ ...prev, minAnswers: Number(event.detail.value || prev.minAnswers) }))} />
|
||||
<Input className='admin-input' placeholder='低正确率阈值' value={String(supervisionRules.lowAccuracyThreshold)} onInput={event => setSupervisionRules(prev => ({ ...prev, lowAccuracyThreshold: Number(event.detail.value || prev.lowAccuracyThreshold) }))} />
|
||||
<Input className='admin-input' placeholder='错题阈值' value={String(supervisionRules.wrongQuestionThreshold)} onInput={event => setSupervisionRules(prev => ({ ...prev, wrongQuestionThreshold: Number(event.detail.value || prev.wrongQuestionThreshold) }))} />
|
||||
<Input className='admin-input' placeholder='待复习单词阈值' value={String(supervisionRules.vocabularyDueThreshold)} onInput={event => setSupervisionRules(prev => ({ ...prev, vocabularyDueThreshold: Number(event.detail.value || prev.vocabularyDueThreshold) }))} />
|
||||
</View>
|
||||
<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>
|
||||
{supervisionResult ? <Text className='admin-row-meta'>{resultSummary(supervisionResult)}</Text> : null}
|
||||
</View>
|
||||
<View className='admin-list'>
|
||||
{supervisionCandidates.slice(0, 6).map(item => (
|
||||
<View className='admin-row' key={item.studentUserId}>
|
||||
<Text className='admin-row-main'>{item.studentName || item.studentUserId} · 风险 {String(item.riskScore || 0)}</Text>
|
||||
<Text className='admin-row-meta'>{item.priority || 'normal'} · {item.description || '学习状态需要跟进'}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!supervisionCandidates.length ? <Text className='admin-row-meta'>可按当前班级范围预览未学习、错题积压、正确率偏低、单词待复习和未完成练习的学生。</Text> : null}
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>CRM 批量推送</Text>
|
||||
<View className='admin-form-grid'>
|
||||
|
||||
Reference in New Issue
Block a user