feat: 对接接口
This commit is contained in:
parent
360834b437
commit
339c871d65
4
.env
4
.env
@ -1,5 +1,5 @@
|
|||||||
VITE_BASE_URL = http://43.136.52.196:9053
|
#VITE_BASE_URL = http://43.136.52.196:9053
|
||||||
# VITE_BASE_URL = https://ai.lingxixue.com
|
VITE_BASE_URL = https://ai.lingxixue.com
|
||||||
|
|
||||||
# VITE_WS_URL = ws://8.138.151.141:8888/
|
# VITE_WS_URL = ws://8.138.151.141:8888/
|
||||||
VITE_WS_URL = ws://ec2-13-36-229-106.eu-west-3.compute.amazonaws.com:8888/
|
VITE_WS_URL = ws://ec2-13-36-229-106.eu-west-3.compute.amazonaws.com:8888/
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
"compileType": "miniprogram",
|
"compileType": "miniprogram",
|
||||||
"appid": "wxa5522671f30c5891",
|
"appid": "wxa5522671f30c5891",
|
||||||
"projectname": "灵犀学",
|
"projectname": "灵犀学",
|
||||||
|
"libVersion": "3.4.4",
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": false,
|
"urlCheck": false,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
@ -14,6 +15,5 @@
|
|||||||
"ignoreUploadUnusedFiles": false,
|
"ignoreUploadUnusedFiles": false,
|
||||||
"minifyWXML": true
|
"minifyWXML": true
|
||||||
},
|
},
|
||||||
"libVersion": "3.4.4",
|
|
||||||
"condition": {}
|
"condition": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,9 +138,50 @@ export const getPaperRecordDetail = (data: { recordId: string; reportFlag: numbe
|
|||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface JobReportVo {
|
||||||
|
actualScore?: number;
|
||||||
|
beginTime?: string;
|
||||||
|
costTime?: number;
|
||||||
|
jobId?: number;
|
||||||
|
jobName?: string;
|
||||||
|
submitEndTime?: string;
|
||||||
|
totalScore?: number;
|
||||||
|
userId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaperChanelAnalyzeItem {
|
||||||
|
chanelTypeName?: string;
|
||||||
|
titleCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaperDifficultyAnalyzeItem {
|
||||||
|
actualScore?: number;
|
||||||
|
difficultyName?: string;
|
||||||
|
/** 百分比比例,乘以 10000,如 9850 表示 98.5% */
|
||||||
|
proportion?: number;
|
||||||
|
score?: number;
|
||||||
|
titleCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StuRankVoItem {
|
||||||
|
markScore?: number;
|
||||||
|
paperScore?: number;
|
||||||
|
rankNum?: number;
|
||||||
|
scoreRate?: number;
|
||||||
|
selfFlag?: number;
|
||||||
|
stuName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JobAnalyseVo {
|
||||||
|
jobReportVo?: JobReportVo;
|
||||||
|
paperChanelAnalyzeList?: PaperChanelAnalyzeItem[];
|
||||||
|
paperDifficultyAnalyzeList?: PaperDifficultyAnalyzeItem[];
|
||||||
|
stuRankVoList?: StuRankVoItem[];
|
||||||
|
}
|
||||||
|
|
||||||
/** 作业整体分析 */
|
/** 作业整体分析 */
|
||||||
export const getJobAnalyse = (params: { recordId: string }) =>
|
export const getJobAnalyse = (params: { recordId: string }) =>
|
||||||
request<any>({
|
request<JobAnalyseVo>({
|
||||||
url: '/sc/job/getJobAnalyse',
|
url: '/sc/job/getJobAnalyse',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params,
|
||||||
|
|||||||
@ -90,6 +90,150 @@ export const manualBindChild = (childAcc: string) =>
|
|||||||
data: `childAcc=${encodeURIComponent(childAcc.trim())}`,
|
data: `childAcc=${encodeURIComponent(childAcc.trim())}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface WrongKpointRatioItem {
|
||||||
|
errorCount?: number;
|
||||||
|
kpointId?: number;
|
||||||
|
kpointName?: string;
|
||||||
|
ratio?: number;
|
||||||
|
ratioPercent?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WrongKpointRatioResult {
|
||||||
|
items?: WrongKpointRatioItem[];
|
||||||
|
subjectId?: number;
|
||||||
|
subjectName?: string;
|
||||||
|
totalErrorCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KpointScoreItem {
|
||||||
|
kpointId?: number;
|
||||||
|
kpointName?: string;
|
||||||
|
actualScore?: number;
|
||||||
|
fullscore?: number;
|
||||||
|
scoreRate?: number;
|
||||||
|
scoreRatePercent?: number;
|
||||||
|
levelCode?: number;
|
||||||
|
levelName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KpointScoreLevel {
|
||||||
|
levelCode?: number;
|
||||||
|
levelName?: string;
|
||||||
|
items?: KpointScoreItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KpointScoreResult {
|
||||||
|
subjectId?: number;
|
||||||
|
subjectName?: string;
|
||||||
|
levels?: KpointScoreLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScoreAbilityPoint {
|
||||||
|
hasData?: boolean;
|
||||||
|
scoreAbility?: number;
|
||||||
|
scoreRate?: number;
|
||||||
|
statDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScoreAbilityResult {
|
||||||
|
assessmentCode?: number;
|
||||||
|
assessmentName?: string;
|
||||||
|
endDate?: string;
|
||||||
|
overallScoreAbility?: number;
|
||||||
|
points?: ScoreAbilityPoint[];
|
||||||
|
stabilityCode?: number;
|
||||||
|
stabilityName?: string;
|
||||||
|
startDate?: string;
|
||||||
|
subjectId?: number;
|
||||||
|
subjectName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WrongQuestionSubjectSummary {
|
||||||
|
subjectId?: number;
|
||||||
|
subjectName?: string;
|
||||||
|
totalCount?: number;
|
||||||
|
masteredCount?: number;
|
||||||
|
reviewCount?: number;
|
||||||
|
newCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WrongQuestionSummaryResult {
|
||||||
|
thisWeekStart?: string;
|
||||||
|
thisWeekEnd?: string;
|
||||||
|
subjects?: WrongQuestionSubjectSummary[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WeeklySubjectTrendCode = 'PROGRESS' | 'REGRESSION' | 'STABLE' | 'NO_COMPARE';
|
||||||
|
|
||||||
|
export interface WeeklySubjectItem {
|
||||||
|
subjectId?: number;
|
||||||
|
subjectName?: string;
|
||||||
|
levelCode?: number;
|
||||||
|
levelName?: string;
|
||||||
|
thisWeekRecordCount?: number;
|
||||||
|
thisWeekScoreRate?: number;
|
||||||
|
lastWeekRecordCount?: number;
|
||||||
|
lastWeekScoreRate?: number | null;
|
||||||
|
delta?: number | null;
|
||||||
|
trendCode?: WeeklySubjectTrendCode | string;
|
||||||
|
trendName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeeklySubjectLevel {
|
||||||
|
levelCode?: number;
|
||||||
|
levelName?: string;
|
||||||
|
subjects?: WeeklySubjectItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeeklySubjectResult {
|
||||||
|
lastWeekEnd?: string;
|
||||||
|
lastWeekStart?: string;
|
||||||
|
thisWeekEnd?: string;
|
||||||
|
thisWeekStart?: string;
|
||||||
|
levels?: WeeklySubjectLevel[];
|
||||||
|
subjects?: WeeklySubjectItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 家长端首页 - 本周学科分层与趋势 */
|
||||||
|
export const getWeeklySubject = (params: { userId: string | number }) =>
|
||||||
|
request<WeeklySubjectResult>({
|
||||||
|
url: '/school/parent/weeklySubject',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 家长端 - 错题汇总(按学科) */
|
||||||
|
export const getWrongQuestionSummary = (params: { userId: string | number }) =>
|
||||||
|
request<WrongQuestionSummaryResult>({
|
||||||
|
url: '/school/parent/wrongQuestionSummary',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 家长端 - 错题知识点占比 */
|
||||||
|
export const getWrongKpointRatio = (params: { subjectId: string | number; userId: string | number }) =>
|
||||||
|
request<WrongKpointRatioResult>({
|
||||||
|
url: '/school/parent/wrongKpointRatio',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 家长端 - 知识点得分(按等级分组) */
|
||||||
|
export const getKpointScore = (params: { subjectId: string | number; userId: string | number }) =>
|
||||||
|
request<KpointScoreResult>({
|
||||||
|
url: '/school/parent/kpointScore',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 家长端 - 得分能力趋势 */
|
||||||
|
export const getScoreAbility = (params: { subjectId: string | number; userId: string | number }) =>
|
||||||
|
request<ScoreAbilityResult>({
|
||||||
|
url: '/school/parent/scoreAbility',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
/** 扫码后申请绑定孩子/设备 */
|
/** 扫码后申请绑定孩子/设备 */
|
||||||
export const parentBindApply = (params: {
|
export const parentBindApply = (params: {
|
||||||
childId?: string | number;
|
childId?: string | number;
|
||||||
|
|||||||
@ -195,7 +195,7 @@
|
|||||||
"role": "parent"
|
"role": "parent"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "成绩查询家长主页",
|
"navigationBarTitleText": "家长主页",
|
||||||
"mp-weixin": {
|
"mp-weixin": {
|
||||||
"pageOrientation": "portrait"
|
"pageOrientation": "portrait"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,32 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<ParentPage active="reports" :show-tab="false">
|
<ParentPage active="reports" :show-tab="false" :use-scroll-view="false">
|
||||||
<ParentFilterRow :items="filterItems" @change="onFilterChange" />
|
<ParentFilterRow :items="filterItems" @change="onFilterChange" />
|
||||||
|
|
||||||
<view class="module-section">
|
<view class="module-section">
|
||||||
<ParentCard title="错题知识点占比" subtitle="按错题占比识别最近需要优先补强的知识点">
|
<ParentCard title="错题知识点占比" subtitle="按错题占比识别最近需要优先补强的知识点">
|
||||||
<view v-if="filteredKnowledge.length" class="ratio-layout">
|
<view v-if="pageLoading" class="loading-wrap">
|
||||||
|
<xy-Empty type="loading" content="加载中..." />
|
||||||
|
</view>
|
||||||
|
<scroll-view
|
||||||
|
v-else-if="wrongRatioList.length"
|
||||||
|
class="ratio-scroll"
|
||||||
|
scroll-y
|
||||||
|
:show-scrollbar="false"
|
||||||
|
:style="ratioScrollStyle"
|
||||||
|
>
|
||||||
|
<view class="ratio-layout">
|
||||||
<view class="ratio-main">
|
<view class="ratio-main">
|
||||||
<view class="ratio-total">
|
<view class="ratio-total">
|
||||||
<text class="ratio-number">{{ mainWrongPercent }}%</text>
|
<text class="ratio-number">{{ mainWrongPercent }}%</text>
|
||||||
<text class="ratio-label">最高错题占比</text>
|
<text class="ratio-label">最高错题占比</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-for="item in filteredKnowledge"
|
v-for="item in wrongRatioList"
|
||||||
:key="`bar-${item.name}`"
|
:key="`bar-${item.kpointId}`"
|
||||||
class="ratio-bar"
|
class="ratio-bar"
|
||||||
>
|
>
|
||||||
<view class="ratio-fill" :class="item.tone" :style="{ width: `${item.wrongPercent}%` }"></view>
|
<view class="ratio-fill" :class="item.tone" :style="{ width: `${item.ratioPercent}%` }"></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="legend-list">
|
<view class="legend-list">
|
||||||
<view v-for="item in filteredKnowledge" :key="item.name" class="legend-item">
|
<view v-for="item in wrongRatioList" :key="item.kpointId" class="legend-item">
|
||||||
<view class="legend-dot" :class="item.tone"></view>
|
<view class="legend-dot" :class="item.tone"></view>
|
||||||
<text class="legend-name ellipsis">{{ item.name }}</text>
|
<text class="legend-name ellipsis">{{ item.kpointName }}</text>
|
||||||
<text class="legend-percent">{{ item.wrongPercent }}%</text>
|
<text class="legend-percent">{{ formatPercent(item.ratioPercent) }}%</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<xy-Empty v-else type="task" content="暂无符合条件的知识点数据" />
|
</scroll-view>
|
||||||
|
<xy-Empty v-else type="task" :content="emptyTip" />
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@ -44,17 +55,22 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="levelKnowledge.length" class="score-table">
|
<view v-if="pageLoading" class="loading-wrap">
|
||||||
|
<xy-Empty type="loading" content="加载中..." />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="levelKnowledge.length" class="score-table-wrap">
|
||||||
<view class="score-row table-head">
|
<view class="score-row table-head">
|
||||||
<text>知识点</text>
|
<text>知识点</text>
|
||||||
<text>满分</text>
|
<text>满分</text>
|
||||||
<text>得分</text>
|
<text>得分</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-for="item in levelKnowledge" :key="`${item.name}-${activeLevel}`" class="score-row">
|
<scroll-view class="score-table-scroll" scroll-y :show-scrollbar="false" :style="scoreTableScrollStyle">
|
||||||
<text class="knowledge-name ellipsis">{{ item.name }}</text>
|
<view v-for="item in levelKnowledge" :key="`${item.kpointId}-${activeLevel}`" class="score-row">
|
||||||
|
<text class="knowledge-name ellipsis">{{ item.kpointName }}</text>
|
||||||
<text>{{ item.fullScore }}</text>
|
<text>{{ item.fullScore }}</text>
|
||||||
<text class="score-value">{{ item.score }}<text class="score-rate">({{ scoreRate(item) }}%)</text></text>
|
<text class="score-value">{{ item.score }}<text class="score-rate">({{ formatPercent(item.scoreRatePercent) }}%)</text></text>
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
<xy-Empty v-else type="learn" content="当前等级暂无知识点" />
|
<xy-Empty v-else type="learn" content="当前等级暂无知识点" />
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
@ -62,16 +78,21 @@
|
|||||||
|
|
||||||
<view class="module-section">
|
<view class="module-section">
|
||||||
<ParentCard title="得分能力" subtitle="综合最近多次练习得分,观察能力变化和稳定性">
|
<ParentCard title="得分能力" subtitle="综合最近多次练习得分,观察能力变化和稳定性">
|
||||||
<view class="ucharts-wrap">
|
<view v-if="pageLoading" class="loading-wrap">
|
||||||
|
<xy-Empty type="loading" content="加载中..." />
|
||||||
|
</view>
|
||||||
|
<template v-else-if="abilityHasContent">
|
||||||
|
<view v-if="abilityHasChartData" class="ucharts-wrap">
|
||||||
<qiun-data-charts
|
<qiun-data-charts
|
||||||
class="ability-chart"
|
class="ability-chart"
|
||||||
type="line"
|
type="line"
|
||||||
canvasId="parentAbilityTrendCanvas2dLineChart"
|
canvasId="parentAbilityTrendCanvas2dLineChart"
|
||||||
background="rgba(255,255,255,0)"
|
background="rgba(255,255,255,0)"
|
||||||
:canvas2d="true"
|
:canvas2d="false"
|
||||||
:in-scroll-view="true"
|
:in-scroll-view="false"
|
||||||
:ontouch="false"
|
:ontouch="false"
|
||||||
:disable-scroll="true"
|
:disable-scroll="true"
|
||||||
|
:reload="chartReload"
|
||||||
:reshow="chartReshow"
|
:reshow="chartReshow"
|
||||||
:opts="abilityChartOpts"
|
:opts="abilityChartOpts"
|
||||||
:chartData="abilityChartData"
|
:chartData="abilityChartData"
|
||||||
@ -79,59 +100,125 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="summary-grid">
|
<view class="summary-grid">
|
||||||
<view v-for="item in abilitySummary" :key="item.label" class="summary-item">
|
<view v-for="item in abilitySummaryList" :key="item.label" class="summary-item">
|
||||||
<text class="summary-label">{{ item.label }}</text>
|
<text class="summary-label">{{ item.label }}</text>
|
||||||
<text class="summary-value">{{ item.value }}</text>
|
<text class="summary-value">{{ item.value }}</text>
|
||||||
<text class="summary-desc">{{ item.desc }}</text>
|
<text class="summary-desc">{{ item.desc }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</template>
|
||||||
|
<xy-Empty v-else type="learn" :content="abilityEmptyTip" />
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
</view>
|
</view>
|
||||||
</ParentPage>
|
</ParentPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
import { onReady, onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import {
|
||||||
|
getKpointScore,
|
||||||
|
getScoreAbility,
|
||||||
|
getWrongKpointRatio,
|
||||||
|
type KpointScoreLevel,
|
||||||
|
type ScoreAbilityPoint,
|
||||||
|
type ScoreAbilityResult,
|
||||||
|
type WrongKpointRatioItem,
|
||||||
|
} from '@/api/parent';
|
||||||
|
import { useParentStore } from '@/state/modules/parent';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
import { formatTime } from '@/utils/format';
|
||||||
|
import { SUBJECT_PICKER_OPTIONS } from '@/utils/subject';
|
||||||
import ParentCard from '../components/ParentCard.vue';
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
import ParentFilterRow from '../components/ParentFilterRow.vue';
|
import ParentFilterRow from '../components/ParentFilterRow.vue';
|
||||||
import ParentPage from '../components/ParentPage.vue';
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
import {
|
|
||||||
abilitySummary,
|
const categoryOptions = ['全部类型', '周作业', '月考'];
|
||||||
abilityTrend,
|
|
||||||
categoryOptions,
|
|
||||||
gradeOptions,
|
|
||||||
knowledgeWeakPoints,
|
|
||||||
subjectOptions,
|
|
||||||
type KnowledgeWeakPoint,
|
|
||||||
} from '../utils/data';
|
|
||||||
|
|
||||||
type ScoreLevel = 'weak' | 'pass' | 'good' | 'excellent';
|
type ScoreLevel = 'weak' | 'pass' | 'good' | 'excellent';
|
||||||
|
type RatioTone = 'blue' | 'orange' | 'green' | 'rose';
|
||||||
|
|
||||||
|
interface WrongRatioViewItem {
|
||||||
|
kpointId: number;
|
||||||
|
kpointName: string;
|
||||||
|
ratioPercent: number;
|
||||||
|
tone: RatioTone;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScoreViewItem {
|
||||||
|
kpointId: number;
|
||||||
|
kpointName: string;
|
||||||
|
fullScore: number;
|
||||||
|
score: number;
|
||||||
|
scoreRatePercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AbilityTrendViewItem {
|
||||||
|
date: string;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AbilitySummaryViewItem {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RATIO_TONES: RatioTone[] = ['blue', 'orange', 'green', 'rose'];
|
||||||
|
|
||||||
|
/** 列表区域高度区间(rpx) */
|
||||||
|
const MODULE_SCROLL_MIN_RPX = 280;
|
||||||
|
const MODULE_SCROLL_MAX_RPX = 520;
|
||||||
|
const SCORE_ROW_RPX = 76;
|
||||||
|
|
||||||
|
const clampModuleScrollHeight = (contentRpx: number) => {
|
||||||
|
const heightRpx = Math.max(MODULE_SCROLL_MIN_RPX, Math.min(MODULE_SCROLL_MAX_RPX, contentRpx));
|
||||||
|
return `${heightRpx}rpx`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LEVEL_CODE_MAP: Record<ScoreLevel, number> = {
|
||||||
|
excellent: 1,
|
||||||
|
good: 2,
|
||||||
|
pass: 3,
|
||||||
|
weak: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const parentStore = useParentStore();
|
||||||
|
const { activeChild } = storeToRefs(parentStore);
|
||||||
|
|
||||||
const subjectIndex = ref(0);
|
const subjectIndex = ref(0);
|
||||||
const gradeIndex = ref(1);
|
|
||||||
const categoryIndex = ref(0);
|
const categoryIndex = ref(0);
|
||||||
const activeLevel = ref<ScoreLevel>('weak');
|
const activeLevel = ref<ScoreLevel>('weak');
|
||||||
|
const pageLoading = ref(false);
|
||||||
|
|
||||||
|
const wrongRatioList = ref<WrongRatioViewItem[]>([]);
|
||||||
|
const scoreLevelsData = ref<KpointScoreLevel[]>([]);
|
||||||
|
const abilityTrendList = ref<AbilityTrendViewItem[]>([]);
|
||||||
|
const abilitySummaryList = ref<AbilitySummaryViewItem[]>([]);
|
||||||
|
const abilityHasContent = ref(false);
|
||||||
|
const abilityHasChartData = ref(false);
|
||||||
|
|
||||||
const filterItems = computed(() => [
|
const filterItems = computed(() => [
|
||||||
{ key: 'subject', range: subjectOptions, value: subjectIndex.value },
|
{ key: 'subject', range: SUBJECT_PICKER_OPTIONS.map((item) => item.label), value: subjectIndex.value },
|
||||||
{ key: 'grade', range: gradeOptions, value: gradeIndex.value },
|
|
||||||
{ key: 'category', range: categoryOptions, value: categoryIndex.value },
|
{ key: 'category', range: categoryOptions, value: categoryIndex.value },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onFilterChange = (payload: { key: string; value: number }) => {
|
const emptyTip = computed(() => {
|
||||||
if (payload.key === 'subject') {
|
if (!activeChild.value.id) return '请先绑定并选择孩子';
|
||||||
subjectIndex.value = payload.value;
|
return '暂无符合条件的知识点数据';
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
if (payload.key === 'grade') {
|
const abilityEmptyTip = computed(() => {
|
||||||
gradeIndex.value = payload.value;
|
if (!activeChild.value.id) return '请先绑定并选择孩子';
|
||||||
return;
|
return '暂无得分能力数据';
|
||||||
}
|
});
|
||||||
if (payload.key === 'category') {
|
|
||||||
categoryIndex.value = payload.value;
|
const mainWrongPercent = computed(() => {
|
||||||
}
|
if (!wrongRatioList.value.length) return 0;
|
||||||
};
|
return Math.max(...wrongRatioList.value.map((item) => item.ratioPercent));
|
||||||
|
});
|
||||||
|
|
||||||
const scoreLevels: { key: ScoreLevel; label: string }[] = [
|
const scoreLevels: { key: ScoreLevel; label: string }[] = [
|
||||||
{ key: 'weak', label: '薄弱' },
|
{ key: 'weak', label: '薄弱' },
|
||||||
@ -140,65 +227,202 @@ const scoreLevels: { key: ScoreLevel; label: string }[] = [
|
|||||||
{ key: 'excellent', label: '优秀' },
|
{ key: 'excellent', label: '优秀' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredKnowledge = computed(() => {
|
const ratioScrollStyle = computed(() => {
|
||||||
const subject = subjectOptions[subjectIndex.value];
|
const count = wrongRatioList.value.length;
|
||||||
const grade = gradeOptions[gradeIndex.value];
|
const mainHeight = 116 + count * 30;
|
||||||
const category = categoryOptions[categoryIndex.value];
|
const legendHeight = count * 46;
|
||||||
return knowledgeWeakPoints.filter((item) => {
|
return { height: clampModuleScrollHeight(Math.max(mainHeight, legendHeight)) };
|
||||||
const subjectMatched = subject === '全部学科' || item.subject === subject;
|
|
||||||
const gradeMatched = grade === '全部年级' || item.grade === grade;
|
|
||||||
const categoryMatched = category === '全部类型' || item.category === category;
|
|
||||||
return subjectMatched && gradeMatched && categoryMatched;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainWrongPercent = computed(() => Math.max(...filteredKnowledge.value.map((item) => item.wrongPercent), 0));
|
const levelKnowledge = computed<ScoreViewItem[]>(() => {
|
||||||
|
const levelCode = LEVEL_CODE_MAP[activeLevel.value];
|
||||||
|
const level = scoreLevelsData.value.find((item) => item.levelCode === levelCode);
|
||||||
|
return (level?.items || []).map((item) => ({
|
||||||
|
kpointId: item.kpointId ?? 0,
|
||||||
|
kpointName: item.kpointName || '--',
|
||||||
|
fullScore: item.fullscore ?? 0,
|
||||||
|
score: item.actualScore ?? 0,
|
||||||
|
scoreRatePercent: item.scoreRatePercent ?? 0,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
const scoreRate = (item: KnowledgeWeakPoint) => Math.round((item.score / item.fullScore) * 100);
|
const scoreTableScrollStyle = computed(() => {
|
||||||
|
const rows = levelKnowledge.value.length;
|
||||||
|
return { height: clampModuleScrollHeight(rows * SCORE_ROW_RPX) };
|
||||||
|
});
|
||||||
|
|
||||||
const levelOf = (item: KnowledgeWeakPoint): ScoreLevel => {
|
const formatPercent = (value?: number) => {
|
||||||
const rate = scoreRate(item);
|
const num = Number(value);
|
||||||
if (rate < 60) return 'weak';
|
if (!Number.isFinite(num)) return '0';
|
||||||
if (rate < 75) return 'pass';
|
return Number.isInteger(num) ? String(num) : num.toFixed(1);
|
||||||
if (rate < 90) return 'good';
|
|
||||||
return 'excellent';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const levelKnowledge = computed(() => filteredKnowledge.value.filter((item) => levelOf(item) === activeLevel.value));
|
const mapWrongRatioItems = (items: WrongKpointRatioItem[] = []): WrongRatioViewItem[] =>
|
||||||
|
items.map((item, index) => ({
|
||||||
|
kpointId: item.kpointId ?? index,
|
||||||
|
kpointName: item.kpointName || '--',
|
||||||
|
ratioPercent: item.ratioPercent ?? 0,
|
||||||
|
tone: RATIO_TONES[index % RATIO_TONES.length],
|
||||||
|
}));
|
||||||
|
|
||||||
const abilityChartData = ref({});
|
const formatAbilityDate = (value?: string) => {
|
||||||
const chartReshow = ref(false);
|
if (!value) return '--';
|
||||||
|
const formatted = formatTime(value, 'MM-DD');
|
||||||
|
return formatted || value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAbilityScore = (item: ScoreAbilityPoint) => {
|
||||||
|
if (!item.hasData) return 0;
|
||||||
|
const score = Number(item.scoreAbility);
|
||||||
|
return Number.isFinite(score) ? score : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapAbilityTrendItems = (points: ScoreAbilityPoint[] = []): AbilityTrendViewItem[] =>
|
||||||
|
points.map((item) => ({
|
||||||
|
date: formatAbilityDate(item.statDate),
|
||||||
|
score: normalizeAbilityScore(item),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const hasAbilityChartData = (points: ScoreAbilityPoint[] = []) => points.length > 0;
|
||||||
|
|
||||||
|
const mapAbilitySummary = (data?: ScoreAbilityResult): AbilitySummaryViewItem[] => {
|
||||||
|
const overallScore = data?.overallScoreAbility;
|
||||||
|
const overallDesc =
|
||||||
|
overallScore != null
|
||||||
|
? `综合得分能力 ${formatPercent(overallScore)} 分`
|
||||||
|
: '暂无评估数据';
|
||||||
|
const periodDesc =
|
||||||
|
data?.startDate && data?.endDate
|
||||||
|
? `统计周期 ${formatAbilityDate(data.startDate)} ~ ${formatAbilityDate(data.endDate)}`
|
||||||
|
: '暂无稳定性分析';
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '得分能力综合评估',
|
||||||
|
value: data?.assessmentName || '--',
|
||||||
|
desc: overallDesc,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '得分能力稳定性',
|
||||||
|
value: data?.stabilityName || '--',
|
||||||
|
desc: periodDesc,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedSubjectId = () => SUBJECT_PICKER_OPTIONS[subjectIndex.value]?.value;
|
||||||
|
|
||||||
|
const fetchAnalysisData = async () => {
|
||||||
|
const userId = activeChild.value.id;
|
||||||
|
const subjectId = getSelectedSubjectId();
|
||||||
|
|
||||||
|
if (!userId || !subjectId) {
|
||||||
|
wrongRatioList.value = [];
|
||||||
|
scoreLevelsData.value = [];
|
||||||
|
abilityTrendList.value = [];
|
||||||
|
abilitySummaryList.value = [];
|
||||||
|
abilityHasContent.value = false;
|
||||||
|
abilityHasChartData.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageLoading.value = true;
|
||||||
|
try {
|
||||||
|
const params = { subjectId, userId };
|
||||||
|
const [ratioRes, scoreRes, abilityRes] = await Promise.all([
|
||||||
|
getWrongKpointRatio(params),
|
||||||
|
getKpointScore(params),
|
||||||
|
getScoreAbility(params),
|
||||||
|
]);
|
||||||
|
wrongRatioList.value = mapWrongRatioItems(ratioRes.data?.items);
|
||||||
|
scoreLevelsData.value = scoreRes.data?.levels || [];
|
||||||
|
abilityTrendList.value = mapAbilityTrendItems(abilityRes.data?.points);
|
||||||
|
abilitySummaryList.value = mapAbilitySummary(abilityRes.data);
|
||||||
|
abilityHasChartData.value = hasAbilityChartData(abilityRes.data?.points);
|
||||||
|
abilityHasContent.value = abilityHasChartData.value || !!abilityRes.data?.assessmentName;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[parent-analysis] fetch err', err);
|
||||||
|
wrongRatioList.value = [];
|
||||||
|
scoreLevelsData.value = [];
|
||||||
|
abilityTrendList.value = [];
|
||||||
|
abilitySummaryList.value = [];
|
||||||
|
abilityHasContent.value = false;
|
||||||
|
abilityHasChartData.value = false;
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false;
|
||||||
|
await refreshChart();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilterChange = (payload: { key: string; value: number }) => {
|
||||||
|
if (payload.key === 'subject') {
|
||||||
|
subjectIndex.value = payload.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (payload.key === 'category') {
|
||||||
|
categoryIndex.value = payload.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch([subjectIndex, categoryIndex, () => activeChild.value.id], () => {
|
||||||
|
fetchAnalysisData();
|
||||||
|
});
|
||||||
|
|
||||||
const buildAbilityChartData = () => ({
|
const buildAbilityChartData = () => ({
|
||||||
categories: abilityTrend.map((item) => item.date),
|
categories: abilityTrendList.value.map((item) => item.date),
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name: '得分能力',
|
name: '得分能力',
|
||||||
data: abilityTrend.map((item) => item.score),
|
data: abilityTrendList.value.map((item) => item.score),
|
||||||
color: '#0891b2',
|
color: '#0891b2',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const fillChart = () => {
|
const abilityChartData = ref<Record<string, unknown>>({ categories: [], series: [] });
|
||||||
|
const chartReshow = ref(false);
|
||||||
|
const chartReload = ref(false);
|
||||||
|
|
||||||
|
const refreshChart = async () => {
|
||||||
|
if (!abilityHasChartData.value) {
|
||||||
|
abilityChartData.value = { categories: [], series: [] };
|
||||||
|
return;
|
||||||
|
}
|
||||||
abilityChartData.value = JSON.parse(JSON.stringify(buildAbilityChartData()));
|
abilityChartData.value = JSON.parse(JSON.stringify(buildAbilityChartData()));
|
||||||
|
await nextTick();
|
||||||
|
setTimeout(() => {
|
||||||
|
chartReload.value = !chartReload.value;
|
||||||
chartReshow.value = !chartReshow.value;
|
chartReshow.value = !chartReshow.value;
|
||||||
|
}, 320);
|
||||||
};
|
};
|
||||||
|
|
||||||
onReady(() => {
|
const abilityChartMin = computed(() => {
|
||||||
setTimeout(fillChart, 500);
|
if (!abilityTrendList.value.length) return 0;
|
||||||
|
const minScore = Math.min(...abilityTrendList.value.map((item) => item.score));
|
||||||
|
return Math.max(0, Math.floor(minScore / 10) * 10 - 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(async () => {
|
||||||
if (Object.keys(abilityChartData.value).length) {
|
try {
|
||||||
chartReshow.value = !chartReshow.value;
|
if (!userStore.userInfo.id) {
|
||||||
|
await userStore.fetchParentUserInfo();
|
||||||
}
|
}
|
||||||
|
await parentStore.fetchBindChildren(userStore.userInfo.id);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
await fetchAnalysisData();
|
||||||
});
|
});
|
||||||
|
|
||||||
const abilityChartOpts = {
|
const abilityChartOpts = computed(() => {
|
||||||
|
const pointCount = abilityTrendList.value.length || 30;
|
||||||
|
const labelCount = Math.min(6, Math.max(4, Math.ceil(pointCount / 5)));
|
||||||
|
|
||||||
|
return {
|
||||||
color: ['#0891b2'],
|
color: ['#0891b2'],
|
||||||
padding: [18, 12, 0, 12],
|
padding: [18, 12, 8, 12],
|
||||||
enableScroll: false,
|
enableScroll: false,
|
||||||
|
dataLabel: false,
|
||||||
legend: { show: false },
|
legend: { show: false },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
disableGrid: false,
|
disableGrid: false,
|
||||||
@ -206,6 +430,7 @@ const abilityChartOpts = {
|
|||||||
fontColor: '#64748b',
|
fontColor: '#64748b',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
boundaryGap: 'justify',
|
boundaryGap: 'justify',
|
||||||
|
labelCount,
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
gridType: 'dash',
|
gridType: 'dash',
|
||||||
@ -213,7 +438,7 @@ const abilityChartOpts = {
|
|||||||
gridColor: '#eef2f7',
|
gridColor: '#eef2f7',
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
min: 40,
|
min: abilityChartMin.value,
|
||||||
max: 100,
|
max: 100,
|
||||||
splitNumber: 4,
|
splitNumber: 4,
|
||||||
fontColor: '#94a3b8',
|
fontColor: '#94a3b8',
|
||||||
@ -237,7 +462,8 @@ const abilityChartOpts = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -253,13 +479,21 @@ const abilityChartOpts = {
|
|||||||
margin-top: 36rpx;
|
margin-top: 36rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-wrap {
|
||||||
|
padding: 24rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ratio-scroll {
|
||||||
|
min-height: 280rpx;
|
||||||
|
max-height: 520rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.ratio-layout {
|
.ratio-layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 28rpx;
|
gap: 28rpx;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratio-main {
|
.ratio-main {
|
||||||
@ -370,12 +604,17 @@ const abilityChartOpts = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-table {
|
.score-table-wrap {
|
||||||
border: 2rpx solid #e2e8f0;
|
border: 2rpx solid #e2e8f0;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.score-table-scroll {
|
||||||
|
min-height: 280rpx;
|
||||||
|
max-height: 520rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.score-row {
|
.score-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 112rpx 150rpx;
|
grid-template-columns: minmax(0, 1fr) 112rpx 150rpx;
|
||||||
|
|||||||
@ -14,20 +14,25 @@
|
|||||||
|
|
||||||
<view class="grade-card">
|
<view class="grade-card">
|
||||||
<ParentCard>
|
<ParentCard>
|
||||||
|
<view v-if="weeklyLoading" class="weekly-loading">
|
||||||
|
<xy-Empty type="loading" content="加载中..." />
|
||||||
|
</view>
|
||||||
|
<template v-else>
|
||||||
<view v-for="band in gradeBands" :key="band.level" class="band-row">
|
<view v-for="band in gradeBands" :key="band.level" class="band-row">
|
||||||
<view class="band-badge" :class="band.level">{{ band.label.slice(0, 2) }}</view>
|
<view class="band-badge" :class="band.level">{{ band.label.slice(0, 2) }}</view>
|
||||||
<view v-if="band.subjects.length" class="subject-list">
|
<view v-if="band.subjects.length" class="subject-list">
|
||||||
<view v-for="item in band.subjects" :key="`${band.level}-${item.subject}`" class="subject-chip">
|
<view v-for="item in band.subjects" :key="`${band.level}-${item.subject}`" class="subject-chip">
|
||||||
<text>{{ item.subject }}</text>
|
<text>{{ item.subject }}</text>
|
||||||
<text class="trend" :class="item.status">{{ trendText(item.status) }}</text>
|
<text v-if="trendLabel(item)" class="trend" :class="item.status">{{ trendLabel(item) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text v-else class="empty-text">暂无需要关注的学科</text>
|
<text v-else class="empty-text">暂无需要关注的学科</text>
|
||||||
</view>
|
</view>
|
||||||
|
</template>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="member-card">
|
<!-- <view class="member-card">
|
||||||
<ParentCard clickable @click="showComingSoon('家长会员')">
|
<ParentCard clickable @click="showComingSoon('家长会员')">
|
||||||
<view class="member-tag">家长会员</view>
|
<view class="member-tag">家长会员</view>
|
||||||
<view class="member-main">
|
<view class="member-main">
|
||||||
@ -38,14 +43,14 @@
|
|||||||
<text class="member-arrow">查看</text>
|
<text class="member-arrow">查看</text>
|
||||||
</view>
|
</view>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
</view>
|
</view> -->
|
||||||
|
|
||||||
<view class="read-card">
|
<!-- <view class="read-card">
|
||||||
<ParentCard title="本周家长必读" subtitle="系统按孩子最近表现生成关注建议">
|
<ParentCard title="本周家长必读" subtitle="系统按孩子最近表现生成关注建议">
|
||||||
<text class="read-text">数学周练后情绪略低,建议先肯定一次认真订正,再安排一条免费话术在「家长关怀」里。</text>
|
<text class="read-text">数学周练后情绪略低,建议先肯定一次认真订正,再安排一条免费话术在「家长关怀」里。</text>
|
||||||
<view class="read-link" @click="showComingSoon('家长关怀')">进入家长关怀中心</view>
|
<view class="read-link" @click="showComingSoon('家长关怀')">进入家长关怀中心</view>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
</view>
|
</view> -->
|
||||||
|
|
||||||
<view class="quick-grid">
|
<view class="quick-grid">
|
||||||
<ParentCard v-for="item in quickActions" :key="item.title" class="quick-card" clickable @click="handleQuickAction(item)">
|
<ParentCard v-for="item in quickActions" :key="item.title" class="quick-card" clickable @click="handleQuickAction(item)">
|
||||||
@ -58,41 +63,117 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { getWeeklySubject, type WeeklySubjectLevel } from '@/api/parent';
|
||||||
import { useParentStore } from '@/state/modules/parent';
|
import { useParentStore } from '@/state/modules/parent';
|
||||||
import { useUserStore } from '@/state/modules/user';
|
import { useUserStore } from '@/state/modules/user';
|
||||||
import ParentCard from '../components/ParentCard.vue';
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
import ParentPage from '../components/ParentPage.vue';
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
import { gradeBands, quickActions, type QuickAction, type SubjectTrend } from '../utils/data';
|
import { quickActions, type GradeBand, type QuickAction, type SubjectTrend } from '../utils/data';
|
||||||
|
|
||||||
const DEFAULT_AVATAR = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/logo.png';
|
const DEFAULT_AVATAR = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/logo.png';
|
||||||
|
|
||||||
|
const LEVEL_BAND_DEFAULTS: GradeBand[] = [
|
||||||
|
{ label: '优势保持', level: 'excellent', subjects: [] },
|
||||||
|
{ label: '良好巩固', level: 'good', subjects: [] },
|
||||||
|
{ label: '重点提升', level: 'middle', subjects: [] },
|
||||||
|
{ label: '及时补弱', level: 'weak', subjects: [] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const LEVEL_CLASS_MAP: Record<number, GradeBand['level']> = {
|
||||||
|
1: 'excellent',
|
||||||
|
2: 'good',
|
||||||
|
3: 'middle',
|
||||||
|
4: 'weak',
|
||||||
|
};
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const parentStore = useParentStore();
|
const parentStore = useParentStore();
|
||||||
const { userInfo } = storeToRefs(userStore);
|
const { userInfo } = storeToRefs(userStore);
|
||||||
const { activeChild } = storeToRefs(parentStore);
|
const { activeChild } = storeToRefs(parentStore);
|
||||||
const parentAvatarUrl = computed(() => userInfo.value.avatarUrl || DEFAULT_AVATAR);
|
const parentAvatarUrl = computed(() => userInfo.value.avatarUrl || DEFAULT_AVATAR);
|
||||||
|
|
||||||
|
const gradeBands = ref<GradeBand[]>(LEVEL_BAND_DEFAULTS.map((item) => ({ ...item, subjects: [] })));
|
||||||
|
const weeklyLoading = ref(false);
|
||||||
|
|
||||||
|
const createEmptyGradeBands = () =>
|
||||||
|
LEVEL_BAND_DEFAULTS.map((item) => ({
|
||||||
|
...item,
|
||||||
|
subjects: [] as SubjectTrend[],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mapTrendStatus = (trendCode?: string): SubjectTrend['status'] => {
|
||||||
|
if (trendCode === 'PROGRESS') return 'up';
|
||||||
|
if (trendCode === 'REGRESSION') return 'down';
|
||||||
|
if (trendCode === 'STABLE') return 'stable';
|
||||||
|
if (trendCode === 'NO_COMPARE') return 'none';
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapLevelsToBands = (levels?: WeeklySubjectLevel[]): GradeBand[] => {
|
||||||
|
const byCode = new Map((levels || []).map((item) => [item.levelCode, item]));
|
||||||
|
return LEVEL_BAND_DEFAULTS.map((fallback, index) => {
|
||||||
|
const levelCode = index + 1;
|
||||||
|
const apiLevel = byCode.get(levelCode);
|
||||||
|
return {
|
||||||
|
label: apiLevel?.levelName || fallback.label,
|
||||||
|
level: LEVEL_CLASS_MAP[levelCode] || fallback.level,
|
||||||
|
subjects: (apiLevel?.subjects || [])
|
||||||
|
.filter((item) => item.subjectName)
|
||||||
|
.map((item) => {
|
||||||
|
const status = mapTrendStatus(item.trendCode);
|
||||||
|
return {
|
||||||
|
subject: item.subjectName || '',
|
||||||
|
status,
|
||||||
|
trendName: item.trendName,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchWeeklySubject = async () => {
|
||||||
|
const userId = activeChild.value.id;
|
||||||
|
if (!userId) {
|
||||||
|
gradeBands.value = createEmptyGradeBands();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
weeklyLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getWeeklySubject({ userId });
|
||||||
|
gradeBands.value = mapLevelsToBands(res.data?.levels);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[parent-home] weeklySubject error', err);
|
||||||
|
gradeBands.value = createEmptyGradeBands();
|
||||||
|
} finally {
|
||||||
|
weeklyLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
try {
|
try {
|
||||||
if (!userInfo.value.id) {
|
if (!userInfo.value.id) {
|
||||||
await userStore.fetchParentUserInfo();
|
await userStore.fetchParentUserInfo();
|
||||||
}
|
}
|
||||||
await parentStore.fetchBindChildren(userInfo.value.id);
|
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||||
|
await fetchWeeklySubject();
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const trendText = (status: SubjectTrend['status']) => {
|
const trendLabel = (item: SubjectTrend) => {
|
||||||
const map: Record<SubjectTrend['status'], string> = {
|
if (item.trendName) return item.trendName;
|
||||||
|
const map: Record<NonNullable<SubjectTrend['status']>, string> = {
|
||||||
up: '进步',
|
up: '进步',
|
||||||
down: '退步',
|
down: '退步',
|
||||||
stable: '稳定',
|
stable: '持平',
|
||||||
|
none: '',
|
||||||
};
|
};
|
||||||
return map[status];
|
return item.status ? map[item.status] : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const showComingSoon = (name: string) => {
|
const showComingSoon = (name: string) => {
|
||||||
@ -230,6 +311,11 @@ const handleQuickAction = (item: QuickAction) => {
|
|||||||
&.up { background: #22c55e; }
|
&.up { background: #22c55e; }
|
||||||
&.down { background: #f43f5e; }
|
&.down { background: #f43f5e; }
|
||||||
&.stable { background: #0891b2; }
|
&.stable { background: #0891b2; }
|
||||||
|
&.none { background: #94a3b8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-loading {
|
||||||
|
padding: 24rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-text {
|
.empty-text {
|
||||||
|
|||||||
@ -42,36 +42,63 @@
|
|||||||
</ParentCard>
|
</ParentCard>
|
||||||
|
|
||||||
<template v-if="activeTab === 'analysis'">
|
<template v-if="activeTab === 'analysis'">
|
||||||
<ParentCard class="section-card" title="题型得分" subtitle="按题型拆解本次卷面得分">
|
<ParentCard class="section-card" title="题型分布" subtitle="按题型拆解本次卷面构成">
|
||||||
<view class="data-table">
|
<view v-if="pageLoading && !analysisRows.length" class="answers-loading">
|
||||||
<view class="table-row table-head">
|
<xy-Empty type="loading" content="加载分析数据..." />
|
||||||
<text>题号</text>
|
|
||||||
<text>题型</text>
|
|
||||||
<text>得分</text>
|
|
||||||
<text>得分率</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view v-for="row in detail.analysisRows" :key="row.range" class="table-row">
|
<view v-else-if="analysisRows.length" class="data-table">
|
||||||
<text>{{ row.range }}</text>
|
<view class="table-row table-head">
|
||||||
|
<text>题型</text>
|
||||||
|
<text>题量</text>
|
||||||
|
<text>占比</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="row in analysisRows" :key="row.type" class="table-row channel-row">
|
||||||
<text>{{ row.type }}</text>
|
<text>{{ row.type }}</text>
|
||||||
<text class="strong">{{ row.score }}</text>
|
<text class="strong">{{ row.count }}</text>
|
||||||
<text class="rate-good">{{ row.rate }}</text>
|
<text class="rate-good">{{ row.rate }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<xy-Empty v-else type="task" content="暂无题型分析数据" />
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
|
|
||||||
<ParentCard class="section-card" title="难度得分" subtitle="关注中难题稳定性,决定后续练习重点">
|
<ParentCard class="section-card" title="难度得分" subtitle="关注中难题稳定性,决定后续练习重点">
|
||||||
<view class="data-table">
|
<view v-if="pageLoading && !difficultyRows.length" class="answers-loading">
|
||||||
|
<xy-Empty type="loading" content="加载分析数据..." />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="difficultyRows.length" class="data-table">
|
||||||
<view class="table-row table-head">
|
<view class="table-row table-head">
|
||||||
<text>难度</text>
|
<text>难度</text>
|
||||||
<text>题号</text>
|
<text>题量</text>
|
||||||
<text>得分</text>
|
<text>得分</text>
|
||||||
<text>得分率</text>
|
<text>得分率</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-for="row in detail.difficultyRows" :key="row.difficulty" class="table-row">
|
<view v-for="row in difficultyRows" :key="row.difficulty" class="table-row">
|
||||||
<text>{{ row.difficulty }}</text>
|
<text>{{ row.difficulty }}</text>
|
||||||
<text>{{ row.range }}</text>
|
<text>{{ row.count }}</text>
|
||||||
<text class="strong">{{ row.score }}</text>
|
<text class="strong">{{ row.score }}</text>
|
||||||
<text :class="row.rate === '100%' ? 'rate-good' : 'rate-warn'">{{ row.rate }}</text>
|
<text :class="row.rateValue >= 80 ? 'rate-good' : 'rate-warn'">{{ row.rate }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<xy-Empty v-else type="task" content="暂无难度分析数据" />
|
||||||
|
</ParentCard>
|
||||||
|
|
||||||
|
<ParentCard
|
||||||
|
v-if="classRankList.length"
|
||||||
|
class="section-card"
|
||||||
|
title="班级排名"
|
||||||
|
subtitle="本次作业班级得分排行"
|
||||||
|
>
|
||||||
|
<view class="rank-list">
|
||||||
|
<view
|
||||||
|
v-for="item in classRankList"
|
||||||
|
:key="`${item.rankNum}-${item.stuName}`"
|
||||||
|
class="rank-list-item"
|
||||||
|
:class="{ self: item.selfFlag === 1 }"
|
||||||
|
>
|
||||||
|
<text class="rank-no">{{ item.rankNum }}</text>
|
||||||
|
<text class="rank-name">{{ item.stuName }}</text>
|
||||||
|
<text class="rank-score">{{ item.markScore }}/{{ item.paperScore }}</text>
|
||||||
|
<text class="rank-rate">{{ formatScoreRate(item.scoreRate) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
@ -281,7 +308,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import { getJobAnalyse, getPaperRecordDetail } from '@/api/homework';
|
import { getJobAnalyse, getPaperRecordDetail, type JobAnalyseVo } from '@/api/homework';
|
||||||
import ParentCard from '../components/ParentCard.vue';
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
import ParentPage from '../components/ParentPage.vue';
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
import {
|
import {
|
||||||
@ -332,7 +359,7 @@ const rawTitleItems = ref<Record<string, unknown>[]>([]);
|
|||||||
const questionSummaries = ref<ReportQuestionSummary[]>([]);
|
const questionSummaries = ref<ReportQuestionSummary[]>([]);
|
||||||
const questionDetailCache = ref<Record<number, ReportQuestion>>({});
|
const questionDetailCache = ref<Record<number, ReportQuestion>>({});
|
||||||
const paperDetail = ref<Record<string, unknown>>({});
|
const paperDetail = ref<Record<string, unknown>>({});
|
||||||
const jobAnalyse = ref<Record<string, unknown>>({});
|
const jobAnalyse = ref<JobAnalyseVo>({});
|
||||||
|
|
||||||
onLoad((query) => {
|
onLoad((query) => {
|
||||||
recordId.value = String(query?.id || '');
|
recordId.value = String(query?.id || '');
|
||||||
@ -346,14 +373,23 @@ onLoad((query) => {
|
|||||||
|
|
||||||
const mockDetail = computed(() => reportDetails[recordId.value] || reportDetails.r1);
|
const mockDetail = computed(() => reportDetails[recordId.value] || reportDetails.r1);
|
||||||
|
|
||||||
const formatRank = (rank?: unknown, total?: unknown) => {
|
|
||||||
if (rank == null || rank === '' || total == null || total === '') return '';
|
const formatScoreRate = (value?: number) => {
|
||||||
return `${rank}/${total}`;
|
if (value == null) return '--';
|
||||||
|
const rate = value <= 1 ? value * 100 : value;
|
||||||
|
return `${Math.round(rate * 10) / 10}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDifficultyScoreRate = (actual?: number, total?: number) => {
|
||||||
|
const score = Number(total) || 0;
|
||||||
|
if (score <= 0) return { text: '--', value: 0 };
|
||||||
|
const rate = ((Number(actual) || 0) / score) * 100;
|
||||||
|
return { text: `${Math.round(rate * 10) / 10}%`, value: rate };
|
||||||
};
|
};
|
||||||
|
|
||||||
const scoreBlocks = computed<ReportScoreBlock[]>(() => {
|
const scoreBlocks = computed<ReportScoreBlock[]>(() => {
|
||||||
const vo = (jobAnalyse.value?.jobReportVo || {}) as Record<string, unknown>;
|
const vo = jobAnalyse.value.jobReportVo || {};
|
||||||
const mark = Number(vo.markScore ?? paperDetail.value.score ?? 0);
|
const mark = Number(vo.actualScore ?? paperDetail.value.score ?? 0);
|
||||||
const total = Number(vo.totalScore ?? paperDetail.value.totalScore ?? 0);
|
const total = Number(vo.totalScore ?? paperDetail.value.totalScore ?? 0);
|
||||||
const rateFromScore =
|
const rateFromScore =
|
||||||
total > 0 ? `${Math.round((mark / total) * 1000) / 10}%` : reportScoreRate.value ? `${reportScoreRate.value}%` : '--';
|
total > 0 ? `${Math.round((mark / total) * 1000) / 10}%` : reportScoreRate.value ? `${reportScoreRate.value}%` : '--';
|
||||||
@ -365,27 +401,58 @@ const scoreBlocks = computed<ReportScoreBlock[]>(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const classRankList = computed(() => jobAnalyse.value.stuRankVoList || []);
|
||||||
|
|
||||||
const ranks = computed(() => {
|
const ranks = computed(() => {
|
||||||
const vo = (jobAnalyse.value?.jobReportVo || {}) as Record<string, unknown>;
|
const rankList = classRankList.value;
|
||||||
const paper = paperDetail.value;
|
const selfRank = rankList.find((item) => item.selfFlag === 1);
|
||||||
const classRank = formatRank(
|
const classRank =
|
||||||
vo.classRank ?? paper.classRank ?? paper.classRanking,
|
selfRank?.rankNum != null && rankList.length
|
||||||
vo.classTotal ?? paper.classTotal ?? paper.classRankTotal,
|
? `${selfRank.rankNum}/${rankList.length}`
|
||||||
);
|
: '';
|
||||||
const gradeRank = formatRank(
|
|
||||||
vo.gradeRank ?? paper.gradeRank ?? paper.gradeRanking,
|
|
||||||
vo.gradeTotal ?? paper.gradeTotal ?? paper.gradeRankTotal,
|
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ label: '班级排名', value: classRank || mockDetail.value.ranks[0]?.value || '--' },
|
{ label: '班级排名', value: classRank || '--' },
|
||||||
{ label: '年级排名', value: gradeRank || mockDetail.value.ranks[1]?.value || '--' },
|
{ label: '年级排名', value: '--' },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const analysisRows = computed(() => {
|
||||||
|
const list = jobAnalyse.value.paperChanelAnalyzeList || [];
|
||||||
|
const totalCount = list.reduce((sum, item) => sum + (Number(item.titleCount) || 0), 0);
|
||||||
|
return list.map((item) => {
|
||||||
|
const count = Number(item.titleCount) || 0;
|
||||||
|
const rate =
|
||||||
|
totalCount > 0 ? `${Math.round((count / totalCount) * 1000) / 10}%` : '--';
|
||||||
|
return {
|
||||||
|
type: item.chanelTypeName || '--',
|
||||||
|
count: String(count),
|
||||||
|
rate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const difficultyRows = computed(() =>
|
||||||
|
(jobAnalyse.value.paperDifficultyAnalyzeList || []).map((item) => {
|
||||||
|
const actual = Number(item.actualScore) || 0;
|
||||||
|
const total = Number(item.score) || 0;
|
||||||
|
const { text, value } = formatDifficultyScoreRate(actual, total);
|
||||||
|
return {
|
||||||
|
difficulty: item.difficultyName || '--',
|
||||||
|
count: String(Number(item.titleCount) || 0),
|
||||||
|
score: total > 0 ? `${actual}/${total}` : String(actual),
|
||||||
|
rate: text,
|
||||||
|
rateValue: value,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const report = computed(() => ({
|
const report = computed(() => ({
|
||||||
category: reportCategory.value || getPaperTestTypeLabelFromPaper(),
|
category: reportCategory.value || getPaperTestTypeLabelFromPaper(),
|
||||||
title: reportName.value || String(paperDetail.value.paperName || '作业详情'),
|
title:
|
||||||
|
reportName.value ||
|
||||||
|
jobAnalyse.value.jobReportVo?.jobName ||
|
||||||
|
String(paperDetail.value.paperName || '作业详情'),
|
||||||
date: reportDate.value,
|
date: reportDate.value,
|
||||||
subject: reportSubject.value || String(paperDetail.value.subjectName || '多科'),
|
subject: reportSubject.value || String(paperDetail.value.subjectName || '多科'),
|
||||||
}));
|
}));
|
||||||
@ -495,7 +562,7 @@ const loadReportData = async () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
paperDetail.value = (detailRes.data || {}) as Record<string, unknown>;
|
paperDetail.value = (detailRes.data || {}) as Record<string, unknown>;
|
||||||
jobAnalyse.value = (analyseRes.data || {}) as Record<string, unknown>;
|
jobAnalyse.value = analyseRes.data || {};
|
||||||
|
|
||||||
const titleList = paperDetail.value.titleVoList || paperDetail.value.subjectTitleVoList;
|
const titleList = paperDetail.value.titleVoList || paperDetail.value.subjectTitleVoList;
|
||||||
rawTitleItems.value = Array.isArray(titleList) ? (titleList as Record<string, unknown>[]) : [];
|
rawTitleItems.value = Array.isArray(titleList) ? (titleList as Record<string, unknown>[]) : [];
|
||||||
@ -705,6 +772,10 @@ const goHome = () => {
|
|||||||
border-radius: 26rpx;
|
border-radius: 26rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-row.channel-row {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr);
|
||||||
@ -1091,6 +1162,49 @@ const goHome = () => {
|
|||||||
padding-left: 24rpx;
|
padding-left: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rank-list {
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-list-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 80rpx 1fr 140rpx 120rpx;
|
||||||
|
min-height: 74rpx;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 24rpx;
|
||||||
|
border-top: 2rpx solid #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-list-item:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-list-item.self {
|
||||||
|
color: #1d4ed8;
|
||||||
|
background: #eff6ff;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-no {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-score,
|
||||||
|
.rank-rate {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-actions {
|
.bottom-actions {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 28rpx;
|
left: 28rpx;
|
||||||
|
|||||||
@ -11,7 +11,8 @@ export interface ParentChild {
|
|||||||
|
|
||||||
export interface SubjectTrend {
|
export interface SubjectTrend {
|
||||||
subject: string;
|
subject: string;
|
||||||
status: 'up' | 'down' | 'stable';
|
status?: 'up' | 'down' | 'stable' | 'none';
|
||||||
|
trendName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GradeBand {
|
export interface GradeBand {
|
||||||
|
|||||||
@ -170,10 +170,10 @@
|
|||||||
<view class="safe-bottom"></view>
|
<view class="safe-bottom"></view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<view class="home-float" @click="goHome">
|
<!-- <view class="home-float" @click="goHome">
|
||||||
<view class="home-roof"></view>
|
<view class="home-roof"></view>
|
||||||
<view class="home-body"></view>
|
<view class="home-body"></view>
|
||||||
</view>
|
</view> -->
|
||||||
|
|
||||||
<view v-if="!pageLoading && filteredQuestions.length" class="bottom-bar">
|
<view v-if="!pageLoading && filteredQuestions.length" class="bottom-bar">
|
||||||
<view class="download-btn" @click="downloadWrongQuestions">一键下载错题</view>
|
<view class="download-btn" @click="downloadWrongQuestions">一键下载错题</view>
|
||||||
@ -183,9 +183,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { getWrongQuestionSummary, type WrongQuestionSummaryResult } from '@/api/parent';
|
||||||
import { getListCompleteRecord, getPaperRecordDetail } from '../../api/homework';
|
import { getListCompleteRecord, getPaperRecordDetail } from '../../api/homework';
|
||||||
import { resource_type_homework } from '@/constants/doWork';
|
import { resource_type_homework } from '@/constants/doWork';
|
||||||
|
import { useParentStore } from '@/state/modules/parent';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
|
||||||
type WrongStatus = 'new' | 'review' | 'mastered';
|
type WrongStatus = 'new' | 'review' | 'mastered';
|
||||||
type TabKey = 'all' | 'new' | 'mastered';
|
type TabKey = 'all' | 'new' | 'mastered';
|
||||||
@ -215,7 +219,13 @@ interface WrongQuestion {
|
|||||||
openPanel: 'mine' | 'analysis';
|
openPanel: 'mine' | 'analysis';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const parentStore = useParentStore();
|
||||||
|
const { userInfo } = storeToRefs(userStore);
|
||||||
|
const { activeChild } = storeToRefs(parentStore);
|
||||||
|
|
||||||
const pageLoading = ref(true);
|
const pageLoading = ref(true);
|
||||||
|
const summaryData = ref<WrongQuestionSummaryResult | null>(null);
|
||||||
const wrongQuestions = ref<WrongQuestion[]>([]);
|
const wrongQuestions = ref<WrongQuestion[]>([]);
|
||||||
const selectedSubject = ref('全部');
|
const selectedSubject = ref('全部');
|
||||||
const activeTab = ref<TabKey>('all');
|
const activeTab = ref<TabKey>('all');
|
||||||
@ -231,21 +241,64 @@ const tabs: { key: TabKey; label: string }[] = [
|
|||||||
|
|
||||||
const subjectTones = ['blue', 'green', 'orange', 'rose', 'violet'];
|
const subjectTones = ['blue', 'green', 'orange', 'rose', 'violet'];
|
||||||
|
|
||||||
const totalWrong = computed(() => wrongQuestions.value.length);
|
const totalWrong = computed(() => {
|
||||||
|
const subjects = summaryData.value?.subjects || [];
|
||||||
|
if (subjects.length) {
|
||||||
|
return subjects.reduce((sum, item) => sum + (item.totalCount || 0), 0);
|
||||||
|
}
|
||||||
|
return wrongQuestions.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
const subjectStats = computed(() => {
|
const buildSubjectStatRow = (
|
||||||
const subjects = ['全部', ...Array.from(new Set(wrongQuestions.value.map((item) => item.subject).filter(Boolean)))];
|
name: string,
|
||||||
return subjects.map((name, index) => {
|
stats: { total: number; newCount: number; reviewCount: number; masteredCount: number },
|
||||||
const list = name === '全部' ? wrongQuestions.value : wrongQuestions.value.filter((item) => item.subject === name);
|
index: number,
|
||||||
return {
|
) => ({
|
||||||
name,
|
name,
|
||||||
shortName: name === '全部' ? '全' : name.slice(0, 1),
|
shortName: name === '全部' ? '全' : name.slice(0, 1),
|
||||||
tone: subjectTones[index % subjectTones.length],
|
tone: subjectTones[index % subjectTones.length],
|
||||||
|
total: stats.total,
|
||||||
|
newCount: stats.newCount,
|
||||||
|
reviewCount: stats.reviewCount,
|
||||||
|
masteredCount: stats.masteredCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
const subjectStats = computed(() => {
|
||||||
|
const apiSubjects = summaryData.value?.subjects || [];
|
||||||
|
if (apiSubjects.length) {
|
||||||
|
const allStats = apiSubjects.reduce<{ total: number; newCount: number; reviewCount: number; masteredCount: number }>(
|
||||||
|
(acc, item) => ({
|
||||||
|
total: acc.total + (item.totalCount || 0),
|
||||||
|
newCount: acc.newCount + (item.newCount || 0),
|
||||||
|
reviewCount: acc.reviewCount + (item.reviewCount || 0),
|
||||||
|
masteredCount: acc.masteredCount + (item.masteredCount || 0),
|
||||||
|
}),
|
||||||
|
{ total: 0, newCount: 0, reviewCount: 0, masteredCount: 0 },
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
buildSubjectStatRow('全部', allStats, 0),
|
||||||
|
...apiSubjects.map((item, index) => buildSubjectStatRow(
|
||||||
|
item.subjectName || '其他',
|
||||||
|
{
|
||||||
|
total: item.totalCount || 0,
|
||||||
|
newCount: item.newCount || 0,
|
||||||
|
reviewCount: item.reviewCount || 0,
|
||||||
|
masteredCount: item.masteredCount || 0,
|
||||||
|
},
|
||||||
|
index + 1,
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const subjects = ['全部', ...Array.from(new Set(wrongQuestions.value.map((item) => item.subject).filter(Boolean)))];
|
||||||
|
return subjects.map((name, index) => {
|
||||||
|
const list = name === '全部' ? wrongQuestions.value : wrongQuestions.value.filter((item) => item.subject === name);
|
||||||
|
return buildSubjectStatRow(name, {
|
||||||
total: list.length,
|
total: list.length,
|
||||||
newCount: list.filter((item) => item.status === 'new').length,
|
newCount: list.filter((item) => item.status === 'new').length,
|
||||||
reviewCount: list.filter((item) => item.inReviewBook).length,
|
reviewCount: list.filter((item) => item.inReviewBook).length,
|
||||||
masteredCount: list.filter((item) => item.status === 'mastered').length,
|
masteredCount: list.filter((item) => item.status === 'mastered').length,
|
||||||
};
|
}, index);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -352,9 +405,53 @@ const mapQuestion = (record: any, item: any, index: number): WrongQuestion => ({
|
|||||||
openPanel: 'analysis',
|
openPanel: 'analysis',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resolveUserId = async () => {
|
||||||
|
try {
|
||||||
|
if (!userInfo.value.id) {
|
||||||
|
await userStore.fetchParentUserInfo();
|
||||||
|
}
|
||||||
|
if (userInfo.value.id) {
|
||||||
|
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeChild.value.id) {
|
||||||
|
return activeChild.value.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!userInfo.value.id) {
|
||||||
|
await userStore.fetchUserInfo();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
|
||||||
|
return userInfo.value.id || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSummary = async () => {
|
||||||
|
const userId = await resolveUserId();
|
||||||
|
if (!userId) {
|
||||||
|
summaryData.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getWrongQuestionSummary({ userId });
|
||||||
|
summaryData.value = res.data || null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[wrong] summary err', err);
|
||||||
|
summaryData.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadWrongQuestions = async () => {
|
const loadWrongQuestions = async () => {
|
||||||
pageLoading.value = true;
|
pageLoading.value = true;
|
||||||
try {
|
try {
|
||||||
|
await loadSummary();
|
||||||
const recordsRes: any = await getListCompleteRecord();
|
const recordsRes: any = await getListCompleteRecord();
|
||||||
const records = Array.isArray(recordsRes?.data) ? recordsRes.data : Array.isArray(recordsRes) ? recordsRes : [];
|
const records = Array.isArray(recordsRes?.data) ? recordsRes.data : Array.isArray(recordsRes) ? recordsRes : [];
|
||||||
const detailRecords = await Promise.all(
|
const detailRecords = await Promise.all(
|
||||||
@ -421,6 +518,20 @@ const goHome = () => {
|
|||||||
uni.reLaunch({ url: '/pages/index/index' });
|
uni.reLaunch({ url: '/pages/index/index' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
|
try {
|
||||||
|
if (!userInfo.value.id) {
|
||||||
|
await userStore.fetchParentUserInfo();
|
||||||
|
}
|
||||||
|
if (userInfo.value.id) {
|
||||||
|
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
await loadSummary();
|
||||||
|
});
|
||||||
|
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
loadWrongQuestions();
|
loadWrongQuestions();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -104,3 +104,9 @@ export const SUBJECT_FILTER_OPTIONS: SubjectFilterOption[] = [
|
|||||||
subjectId: String(item.value),
|
subjectId: String(item.value),
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** 学科选择器选项(不含「全部学科」,默认选中第一项) */
|
||||||
|
export const SUBJECT_PICKER_OPTIONS: SubjectFilterOption[] = SUBJECT_OPTIONS.map((item) => ({
|
||||||
|
...item,
|
||||||
|
subjectId: String(item.value),
|
||||||
|
}));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user