forked from wangziqi/gongxue-base
feat: add taro student learning flow
This commit is contained in:
@@ -7,6 +7,8 @@ export interface ContentEntry {
|
||||
route?: string;
|
||||
icon?: string;
|
||||
sortOrder?: number;
|
||||
description?: string;
|
||||
regionId?: string | null;
|
||||
}
|
||||
|
||||
export interface DashboardSnapshot {
|
||||
@@ -30,3 +32,169 @@ export async function loadStudentDashboard(regionId?: string): Promise<Dashboard
|
||||
profile: profile.item || null,
|
||||
};
|
||||
}
|
||||
|
||||
export interface RegionItem {
|
||||
id: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
shortName?: string;
|
||||
isHot?: boolean;
|
||||
}
|
||||
|
||||
export interface ContentNode {
|
||||
id: string;
|
||||
entryId: string;
|
||||
parentId?: string | null;
|
||||
name: string;
|
||||
nodeType?: string;
|
||||
markerType?: string | null;
|
||||
depth?: number;
|
||||
isLeaf?: boolean;
|
||||
isSelectable?: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface QuestionCollection {
|
||||
id: string;
|
||||
entryId?: string | null;
|
||||
nodeId?: string | null;
|
||||
name: string;
|
||||
collectionType?: string;
|
||||
questionCount?: number;
|
||||
durationMinutes?: number | null;
|
||||
}
|
||||
|
||||
export interface PracticeBlueprint {
|
||||
id: string;
|
||||
entryId?: string | null;
|
||||
nodeId?: string | null;
|
||||
collectionId?: string | null;
|
||||
name: string;
|
||||
mode: string;
|
||||
questionLimit?: number | null;
|
||||
durationMinutes?: number | null;
|
||||
}
|
||||
|
||||
export interface VocabularyUnit {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
wordCount?: number;
|
||||
regionId?: string | null;
|
||||
}
|
||||
|
||||
export interface HandbookSubject {
|
||||
id: string;
|
||||
name: string;
|
||||
type?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface HandbookChapter {
|
||||
id: string;
|
||||
subjectId: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface HandbookEntry {
|
||||
id: string;
|
||||
chapterId: string;
|
||||
title: string;
|
||||
summary?: string | null;
|
||||
content?: string | null;
|
||||
}
|
||||
|
||||
export interface ScorelineRecord {
|
||||
id: string;
|
||||
year: number;
|
||||
schoolName?: string | null;
|
||||
majorName?: string | null;
|
||||
fieldValues?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ContentAsset {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
assetType?: string;
|
||||
categoryLabel?: string | null;
|
||||
description?: string | null;
|
||||
fileName?: string | null;
|
||||
mimeType?: string | null;
|
||||
visibility?: string;
|
||||
previewStatus?: string;
|
||||
}
|
||||
|
||||
export async function loadRegions() {
|
||||
return apiRequest<{ items?: RegionItem[] }>('/api/catalog/regions');
|
||||
}
|
||||
|
||||
export async function loadContentEntries(regionId?: string, entryType?: string) {
|
||||
return apiRequest<{ items?: ContentEntry[] }>('/api/catalog/content-entries', { query: { regionId, entryType } });
|
||||
}
|
||||
|
||||
export async function loadContentNodes(entryId: string, parentId: string | null = 'root', mode: 'children' | 'flat' = 'children') {
|
||||
return apiRequest<{ items?: ContentNode[] }>('/api/catalog/content-nodes', { query: { entryId, parentId, mode } });
|
||||
}
|
||||
|
||||
export async function loadQuestionCollections(input: { entryId?: string; nodeId?: string; limit?: number }) {
|
||||
return apiRequest<{ items?: QuestionCollection[] }>('/api/catalog/question-collections', {
|
||||
query: { entryId: input.entryId, nodeId: input.nodeId, limit: input.limit || 100 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadCollectionQuestions(collectionId: string, limit = 200) {
|
||||
return apiRequest<{ items?: import('./learning').QuestionItem[] }>('/api/catalog/question-collections/questions', {
|
||||
query: { collectionId, limit },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadQuestions(query: { entryId?: string; contentNodeId?: string; collectionId?: string; limit?: number }) {
|
||||
return apiRequest<{ items?: import('./learning').QuestionItem[] }>('/api/catalog/questions', {
|
||||
query: { ...query, limit: query.limit || 200 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadPracticeBlueprints(input: { entryId?: string; nodeId?: string; collectionId?: string; mode?: string }) {
|
||||
return apiRequest<{ items?: PracticeBlueprint[] }>('/api/catalog/practice-blueprints', {
|
||||
query: input,
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadVocabularyUnits(regionId?: string) {
|
||||
return apiRequest<{ items?: VocabularyUnit[] }>('/api/catalog/vocabulary-units', { query: { regionId } });
|
||||
}
|
||||
|
||||
export async function loadVocabularyWords(unitId: string) {
|
||||
return apiRequest<{ items?: import('./learning').VocabularyWord[] }>('/api/catalog/vocabulary-words', { query: { unitId } });
|
||||
}
|
||||
|
||||
export async function loadHandbookSubjects(regionId?: string) {
|
||||
return apiRequest<{ items?: HandbookSubject[] }>('/api/catalog/handbook-subjects', { query: { regionId } });
|
||||
}
|
||||
|
||||
export async function loadHandbookChapters(subjectId: string) {
|
||||
return apiRequest<{ items?: HandbookChapter[] }>('/api/catalog/handbook-chapters', { query: { subjectId } });
|
||||
}
|
||||
|
||||
export async function loadHandbookEntries(chapterId: string, includeContent = true) {
|
||||
return apiRequest<{ items?: HandbookEntry[] }>('/api/catalog/handbook-entries', { query: { chapterId, includeContent } });
|
||||
}
|
||||
|
||||
export async function loadScorelineRecords(query: { regionId?: string; schoolId?: string; majorId?: string; year?: number; pageSize?: number } = {}) {
|
||||
return apiRequest<{ items?: ScorelineRecord[]; total?: number }>('/api/scoreline/records', {
|
||||
query: { ...query, pageSize: query.pageSize || 20 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadContentAssets(query: { entryId?: string; contentNodeId?: string; assetType?: string; includeLocked?: boolean } = {}) {
|
||||
return apiRequest<{ items?: ContentAsset[] }>('/api/catalog/assets', { query: { ...query, includeLocked: query.includeLocked ?? true } });
|
||||
}
|
||||
|
||||
export async function signAssetPreview(assetId: string) {
|
||||
return apiRequest<{ preview?: { url?: string }; item?: ContentAsset }>('/api/catalog/assets/preview', { query: { assetId } });
|
||||
}
|
||||
|
||||
export async function signAssetDownload(assetId: string) {
|
||||
return apiRequest<{ download?: { url?: string }; item?: ContentAsset }>('/api/catalog/assets/download', { query: { assetId } });
|
||||
}
|
||||
|
||||
63
apps/taro/src/services/commerce.ts
Normal file
63
apps/taro/src/services/commerce.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { apiRequest } from './api';
|
||||
|
||||
export interface SvipPlan {
|
||||
id: string;
|
||||
name: string;
|
||||
price?: number;
|
||||
priceCents?: number;
|
||||
originalPrice?: number | null;
|
||||
days?: number;
|
||||
desc?: string | null;
|
||||
badge?: string | null;
|
||||
recommended?: boolean;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
id: string;
|
||||
orderNo: string;
|
||||
status: string;
|
||||
productName?: string | null;
|
||||
amountCents?: number;
|
||||
paidAt?: string | null;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export async function loadSvipPlans(regionId?: string) {
|
||||
return apiRequest<{ items?: SvipPlan[] }>('/api/catalog/svip-plans', { query: { regionId } });
|
||||
}
|
||||
|
||||
export async function createOrder(body: {
|
||||
planId: string;
|
||||
regionId?: string;
|
||||
payMethod?: string;
|
||||
payProvider?: string;
|
||||
quantity?: number;
|
||||
couponCode?: string;
|
||||
}) {
|
||||
return apiRequest<{ item?: Record<string, unknown> }>('/api/commerce/orders', {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadOrders(limit = 20) {
|
||||
return apiRequest<{ items?: OrderItem[] }>('/api/commerce/orders', { query: { limit } });
|
||||
}
|
||||
|
||||
export async function loadEntitlements() {
|
||||
return apiRequest<{ items?: Record<string, unknown>[]; summary?: Record<string, unknown> }>('/api/commerce/entitlements');
|
||||
}
|
||||
|
||||
export async function checkActivationCode(code: string, regionId?: string) {
|
||||
return apiRequest<{ valid?: boolean; item?: Record<string, unknown>; reasonCode?: string; message?: string }>(
|
||||
'/api/commerce/activation-codes/check',
|
||||
{ method: 'POST', body: { code, regionId } },
|
||||
);
|
||||
}
|
||||
|
||||
export async function redeemActivationCode(code: string, regionId?: string) {
|
||||
return apiRequest<{ item?: Record<string, unknown>; entitlement?: unknown }>('/api/commerce/activation-codes/redeem', {
|
||||
method: 'POST',
|
||||
body: { code, regionId },
|
||||
});
|
||||
}
|
||||
129
apps/taro/src/services/learning.ts
Normal file
129
apps/taro/src/services/learning.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { apiRequest } from './api';
|
||||
|
||||
export interface QuestionOption {
|
||||
text?: string;
|
||||
label?: string;
|
||||
value?: string | number;
|
||||
content?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface QuestionItem {
|
||||
id: string;
|
||||
content?: string;
|
||||
type?: string;
|
||||
typeLabel?: string;
|
||||
options?: QuestionOption[] | string[] | null;
|
||||
answerText?: string | null;
|
||||
correctOptionIndex?: number | null;
|
||||
correctOptionIndices?: number[] | null;
|
||||
explanation?: string | null;
|
||||
hasVideoExplanation?: boolean;
|
||||
}
|
||||
|
||||
export interface PracticeSession {
|
||||
id: string;
|
||||
mode: string;
|
||||
questionIds: string[];
|
||||
questionCount: number;
|
||||
durationMinutes?: number | null;
|
||||
accessMode?: string;
|
||||
consumedFreeQuota?: number;
|
||||
expiresAt?: string | null;
|
||||
}
|
||||
|
||||
export interface AnswerResult {
|
||||
id: string;
|
||||
questionId: string;
|
||||
isCorrect: boolean | null;
|
||||
}
|
||||
|
||||
export interface VocabularyWord {
|
||||
id?: string;
|
||||
wordId?: string;
|
||||
unitId?: string;
|
||||
word: string;
|
||||
phonetic?: string | null;
|
||||
meaning?: string | null;
|
||||
example?: string | null;
|
||||
exampleTranslation?: string | null;
|
||||
difficulty?: string | null;
|
||||
status?: string;
|
||||
dueLevel?: string;
|
||||
}
|
||||
|
||||
export async function createPracticeSession(body: {
|
||||
blueprintId?: string;
|
||||
collectionId?: string;
|
||||
entryId?: string;
|
||||
contentNodeId?: string;
|
||||
mode?: string;
|
||||
questionLimit?: number;
|
||||
}) {
|
||||
return apiRequest<{ item: PracticeSession }>('/api/learning/practice-sessions', {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function submitAnswer(body: {
|
||||
practiceSessionId: string;
|
||||
questionId: string;
|
||||
selectedOptions?: string[];
|
||||
answerText?: string;
|
||||
}) {
|
||||
return apiRequest<{ item: AnswerResult }>('/api/learning/answers', {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
export async function toggleQuestionFavorite(questionId: string, favorite: boolean) {
|
||||
return apiRequest<{ ok: boolean; favorite: boolean }>('/api/learning/favorites/questions', {
|
||||
method: 'POST',
|
||||
body: { questionId, favorite },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadWrongQuestions(limit = 30) {
|
||||
return apiRequest<{ items?: QuestionItem[] }>('/api/learning/wrong-questions', { query: { limit } });
|
||||
}
|
||||
|
||||
export async function loadFavoriteQuestions(limit = 30) {
|
||||
return apiRequest<{ items?: QuestionItem[] }>('/api/learning/favorites/questions', { query: { limit } });
|
||||
}
|
||||
|
||||
export async function loadLearningStats() {
|
||||
return apiRequest<{ item?: Record<string, unknown> }>('/api/learning/stats');
|
||||
}
|
||||
|
||||
export async function loadLeaderboard(metric: 'questions' | 'score' | 'vocabulary' | 'mock_exam' = 'questions') {
|
||||
return apiRequest<{ items?: Record<string, unknown>[]; currentUserRank?: unknown }>('/api/learning/leaderboard', {
|
||||
query: { metric, period: '7d', scope: 'tenant' },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadVocabularyReviewPlan(unitId?: string) {
|
||||
return apiRequest<{ item?: { words?: VocabularyWord[]; dueCount?: number; newCount?: number; totalPlanned?: number } }>(
|
||||
'/api/learning/vocabulary/review-plan',
|
||||
{ query: { unitId, reviewLimit: 20, newLimit: 10 } },
|
||||
);
|
||||
}
|
||||
|
||||
export async function reviewVocabularyWord(wordId: string, result: 'known' | 'unknown') {
|
||||
return apiRequest<{ item?: Record<string, unknown> }>('/api/learning/vocabulary/review', {
|
||||
method: 'POST',
|
||||
body: { wordId, result },
|
||||
});
|
||||
}
|
||||
|
||||
export async function toggleWordFavorite(wordId: string, favorite: boolean) {
|
||||
return apiRequest<{ ok: boolean; favorite: boolean }>('/api/learning/vocabulary/favorites', {
|
||||
method: 'POST',
|
||||
body: { wordId, favorite },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadVocabularyStats(unitId?: string) {
|
||||
return apiRequest<{ item?: Record<string, unknown> }>('/api/learning/vocabulary/stats', { query: { unitId } });
|
||||
}
|
||||
35
apps/taro/src/services/profile.ts
Normal file
35
apps/taro/src/services/profile.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { apiRequest } from './api';
|
||||
|
||||
export interface StudentProfile {
|
||||
userId?: string;
|
||||
name?: string | null;
|
||||
phone?: string | null;
|
||||
score?: number;
|
||||
target?: {
|
||||
regionName?: string | null;
|
||||
schoolName?: string | null;
|
||||
majorName?: string | null;
|
||||
};
|
||||
membership?: {
|
||||
isSvip?: boolean;
|
||||
entitlement?: unknown;
|
||||
};
|
||||
stats?: Record<string, unknown>;
|
||||
recentPractices?: unknown[];
|
||||
}
|
||||
|
||||
export async function loadProfile() {
|
||||
return apiRequest<{ item?: StudentProfile }>('/api/profile/me');
|
||||
}
|
||||
|
||||
export async function checkIn() {
|
||||
return apiRequest<{ item?: Record<string, unknown> }>('/api/profile/check-in', { method: 'POST' });
|
||||
}
|
||||
|
||||
export async function loadBadges() {
|
||||
return apiRequest<{ items?: Record<string, unknown>[] }>('/api/profile/badges');
|
||||
}
|
||||
|
||||
export async function loadExamCountdowns() {
|
||||
return apiRequest<{ items?: Record<string, unknown>[] }>('/api/profile/exam-countdowns');
|
||||
}
|
||||
Reference in New Issue
Block a user