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 = https://ai.lingxixue.com
|
||||
#VITE_BASE_URL = http://43.136.52.196:9053
|
||||
VITE_BASE_URL = https://ai.lingxixue.com
|
||||
|
||||
# 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/
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"compileType": "miniprogram",
|
||||
"appid": "wxa5522671f30c5891",
|
||||
"projectname": "灵犀学",
|
||||
"libVersion": "3.4.4",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
@ -14,6 +15,5 @@
|
||||
"ignoreUploadUnusedFiles": false,
|
||||
"minifyWXML": true
|
||||
},
|
||||
"libVersion": "3.4.4",
|
||||
"condition": {}
|
||||
}
|
||||
|
||||
@ -138,9 +138,50 @@ export const getPaperRecordDetail = (data: { recordId: string; reportFlag: numbe
|
||||
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 }) =>
|
||||
request<any>({
|
||||
request<JobAnalyseVo>({
|
||||
url: '/sc/job/getJobAnalyse',
|
||||
method: 'GET',
|
||||
params,
|
||||
|
||||
@ -90,6 +90,150 @@ export const manualBindChild = (childAcc: string) =>
|
||||
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: {
|
||||
childId?: string | number;
|
||||
|
||||
@ -195,7 +195,7 @@
|
||||
"role": "parent"
|
||||
},
|
||||
"style": {
|
||||
"navigationBarTitleText": "成绩查询家长主页",
|
||||
"navigationBarTitleText": "家长主页",
|
||||
"mp-weixin": {
|
||||
"pageOrientation": "portrait"
|
||||
}
|
||||
|
||||
@ -1,32 +1,43 @@
|
||||
<template>
|
||||
<ParentPage active="reports" :show-tab="false">
|
||||
<ParentPage active="reports" :show-tab="false" :use-scroll-view="false">
|
||||
<ParentFilterRow :items="filterItems" @change="onFilterChange" />
|
||||
|
||||
<view class="module-section">
|
||||
<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-total">
|
||||
<text class="ratio-number">{{ mainWrongPercent }}%</text>
|
||||
<text class="ratio-label">最高错题占比</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="item in filteredKnowledge"
|
||||
:key="`bar-${item.name}`"
|
||||
v-for="item in wrongRatioList"
|
||||
:key="`bar-${item.kpointId}`"
|
||||
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 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>
|
||||
<text class="legend-name ellipsis">{{ item.name }}</text>
|
||||
<text class="legend-percent">{{ item.wrongPercent }}%</text>
|
||||
<text class="legend-name ellipsis">{{ item.kpointName }}</text>
|
||||
<text class="legend-percent">{{ formatPercent(item.ratioPercent) }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<xy-Empty v-else type="task" content="暂无符合条件的知识点数据" />
|
||||
</scroll-view>
|
||||
<xy-Empty v-else type="task" :content="emptyTip" />
|
||||
</ParentCard>
|
||||
</view>
|
||||
|
||||
@ -44,17 +55,22 @@
|
||||
</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">
|
||||
<text>知识点</text>
|
||||
<text>满分</text>
|
||||
<text>得分</text>
|
||||
</view>
|
||||
<view v-for="item in levelKnowledge" :key="`${item.name}-${activeLevel}`" class="score-row">
|
||||
<text class="knowledge-name ellipsis">{{ item.name }}</text>
|
||||
<scroll-view class="score-table-scroll" scroll-y :show-scrollbar="false" :style="scoreTableScrollStyle">
|
||||
<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 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>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<xy-Empty v-else type="learn" content="当前等级暂无知识点" />
|
||||
</ParentCard>
|
||||
@ -62,16 +78,21 @@
|
||||
|
||||
<view class="module-section">
|
||||
<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
|
||||
class="ability-chart"
|
||||
type="line"
|
||||
canvasId="parentAbilityTrendCanvas2dLineChart"
|
||||
background="rgba(255,255,255,0)"
|
||||
:canvas2d="true"
|
||||
:in-scroll-view="true"
|
||||
:canvas2d="false"
|
||||
:in-scroll-view="false"
|
||||
:ontouch="false"
|
||||
:disable-scroll="true"
|
||||
:reload="chartReload"
|
||||
:reshow="chartReshow"
|
||||
:opts="abilityChartOpts"
|
||||
:chartData="abilityChartData"
|
||||
@ -79,59 +100,125 @@
|
||||
</view>
|
||||
|
||||
<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-value">{{ item.value }}</text>
|
||||
<text class="summary-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<xy-Empty v-else type="learn" :content="abilityEmptyTip" />
|
||||
</ParentCard>
|
||||
</view>
|
||||
</ParentPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onReady, onShow } from '@dcloudio/uni-app';
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
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 ParentFilterRow from '../components/ParentFilterRow.vue';
|
||||
import ParentPage from '../components/ParentPage.vue';
|
||||
import {
|
||||
abilitySummary,
|
||||
abilityTrend,
|
||||
categoryOptions,
|
||||
gradeOptions,
|
||||
knowledgeWeakPoints,
|
||||
subjectOptions,
|
||||
type KnowledgeWeakPoint,
|
||||
} from '../utils/data';
|
||||
|
||||
const categoryOptions = ['全部类型', '周作业', '月考'];
|
||||
|
||||
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 gradeIndex = ref(1);
|
||||
const categoryIndex = ref(0);
|
||||
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(() => [
|
||||
{ key: 'subject', range: subjectOptions, value: subjectIndex.value },
|
||||
{ key: 'grade', range: gradeOptions, value: gradeIndex.value },
|
||||
{ key: 'subject', range: SUBJECT_PICKER_OPTIONS.map((item) => item.label), value: subjectIndex.value },
|
||||
{ key: 'category', range: categoryOptions, value: categoryIndex.value },
|
||||
]);
|
||||
|
||||
const onFilterChange = (payload: { key: string; value: number }) => {
|
||||
if (payload.key === 'subject') {
|
||||
subjectIndex.value = payload.value;
|
||||
return;
|
||||
}
|
||||
if (payload.key === 'grade') {
|
||||
gradeIndex.value = payload.value;
|
||||
return;
|
||||
}
|
||||
if (payload.key === 'category') {
|
||||
categoryIndex.value = payload.value;
|
||||
}
|
||||
};
|
||||
const emptyTip = computed(() => {
|
||||
if (!activeChild.value.id) return '请先绑定并选择孩子';
|
||||
return '暂无符合条件的知识点数据';
|
||||
});
|
||||
|
||||
const abilityEmptyTip = computed(() => {
|
||||
if (!activeChild.value.id) return '请先绑定并选择孩子';
|
||||
return '暂无得分能力数据';
|
||||
});
|
||||
|
||||
const mainWrongPercent = computed(() => {
|
||||
if (!wrongRatioList.value.length) return 0;
|
||||
return Math.max(...wrongRatioList.value.map((item) => item.ratioPercent));
|
||||
});
|
||||
|
||||
const scoreLevels: { key: ScoreLevel; label: string }[] = [
|
||||
{ key: 'weak', label: '薄弱' },
|
||||
@ -140,65 +227,202 @@ const scoreLevels: { key: ScoreLevel; label: string }[] = [
|
||||
{ key: 'excellent', label: '优秀' },
|
||||
];
|
||||
|
||||
const filteredKnowledge = computed(() => {
|
||||
const subject = subjectOptions[subjectIndex.value];
|
||||
const grade = gradeOptions[gradeIndex.value];
|
||||
const category = categoryOptions[categoryIndex.value];
|
||||
return knowledgeWeakPoints.filter((item) => {
|
||||
const subjectMatched = subject === '全部学科' || item.subject === subject;
|
||||
const gradeMatched = grade === '全部年级' || item.grade === grade;
|
||||
const categoryMatched = category === '全部类型' || item.category === category;
|
||||
return subjectMatched && gradeMatched && categoryMatched;
|
||||
});
|
||||
const ratioScrollStyle = computed(() => {
|
||||
const count = wrongRatioList.value.length;
|
||||
const mainHeight = 116 + count * 30;
|
||||
const legendHeight = count * 46;
|
||||
return { height: clampModuleScrollHeight(Math.max(mainHeight, legendHeight)) };
|
||||
});
|
||||
|
||||
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 rate = scoreRate(item);
|
||||
if (rate < 60) return 'weak';
|
||||
if (rate < 75) return 'pass';
|
||||
if (rate < 90) return 'good';
|
||||
return 'excellent';
|
||||
const formatPercent = (value?: number) => {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return '0';
|
||||
return Number.isInteger(num) ? String(num) : num.toFixed(1);
|
||||
};
|
||||
|
||||
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 chartReshow = ref(false);
|
||||
const formatAbilityDate = (value?: string) => {
|
||||
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 = () => ({
|
||||
categories: abilityTrend.map((item) => item.date),
|
||||
categories: abilityTrendList.value.map((item) => item.date),
|
||||
series: [
|
||||
{
|
||||
name: '得分能力',
|
||||
data: abilityTrend.map((item) => item.score),
|
||||
data: abilityTrendList.value.map((item) => item.score),
|
||||
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()));
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
chartReload.value = !chartReload.value;
|
||||
chartReshow.value = !chartReshow.value;
|
||||
}, 320);
|
||||
};
|
||||
|
||||
onReady(() => {
|
||||
setTimeout(fillChart, 500);
|
||||
const abilityChartMin = computed(() => {
|
||||
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(() => {
|
||||
if (Object.keys(abilityChartData.value).length) {
|
||||
chartReshow.value = !chartReshow.value;
|
||||
onShow(async () => {
|
||||
try {
|
||||
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'],
|
||||
padding: [18, 12, 0, 12],
|
||||
padding: [18, 12, 8, 12],
|
||||
enableScroll: false,
|
||||
dataLabel: false,
|
||||
legend: { show: false },
|
||||
xAxis: {
|
||||
disableGrid: false,
|
||||
@ -206,6 +430,7 @@ const abilityChartOpts = {
|
||||
fontColor: '#64748b',
|
||||
fontSize: 10,
|
||||
boundaryGap: 'justify',
|
||||
labelCount,
|
||||
},
|
||||
yAxis: {
|
||||
gridType: 'dash',
|
||||
@ -213,7 +438,7 @@ const abilityChartOpts = {
|
||||
gridColor: '#eef2f7',
|
||||
data: [
|
||||
{
|
||||
min: 40,
|
||||
min: abilityChartMin.value,
|
||||
max: 100,
|
||||
splitNumber: 4,
|
||||
fontColor: '#94a3b8',
|
||||
@ -237,7 +462,8 @@ const abilityChartOpts = {
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@ -253,13 +479,21 @@ const abilityChartOpts = {
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
|
||||
.loading-wrap {
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.ratio-scroll {
|
||||
min-height: 280rpx;
|
||||
max-height: 520rpx;
|
||||
}
|
||||
|
||||
.ratio-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 28rpx;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ratio-main {
|
||||
@ -370,12 +604,17 @@ const abilityChartOpts = {
|
||||
}
|
||||
}
|
||||
|
||||
.score-table {
|
||||
.score-table-wrap {
|
||||
border: 2rpx solid #e2e8f0;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.score-table-scroll {
|
||||
min-height: 280rpx;
|
||||
max-height: 520rpx;
|
||||
}
|
||||
|
||||
.score-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 112rpx 150rpx;
|
||||
|
||||
@ -14,20 +14,25 @@
|
||||
|
||||
<view class="grade-card">
|
||||
<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 class="band-badge" :class="band.level">{{ band.label.slice(0, 2) }}</view>
|
||||
<view v-if="band.subjects.length" class="subject-list">
|
||||
<view v-for="item in band.subjects" :key="`${band.level}-${item.subject}`" class="subject-chip">
|
||||
<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>
|
||||
<text v-else class="empty-text">暂无需要关注的学科</text>
|
||||
</view>
|
||||
</template>
|
||||
</ParentCard>
|
||||
</view>
|
||||
|
||||
<view class="member-card">
|
||||
<!-- <view class="member-card">
|
||||
<ParentCard clickable @click="showComingSoon('家长会员')">
|
||||
<view class="member-tag">家长会员</view>
|
||||
<view class="member-main">
|
||||
@ -38,14 +43,14 @@
|
||||
<text class="member-arrow">查看</text>
|
||||
</view>
|
||||
</ParentCard>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<view class="read-card">
|
||||
<!-- <view class="read-card">
|
||||
<ParentCard title="本周家长必读" subtitle="系统按孩子最近表现生成关注建议">
|
||||
<text class="read-text">数学周练后情绪略低,建议先肯定一次认真订正,再安排一条免费话术在「家长关怀」里。</text>
|
||||
<view class="read-link" @click="showComingSoon('家长关怀')">进入家长关怀中心</view>
|
||||
</ParentCard>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<view class="quick-grid">
|
||||
<ParentCard v-for="item in quickActions" :key="item.title" class="quick-card" clickable @click="handleQuickAction(item)">
|
||||
@ -58,41 +63,117 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { getWeeklySubject, type WeeklySubjectLevel } from '@/api/parent';
|
||||
import { useParentStore } from '@/state/modules/parent';
|
||||
import { useUserStore } from '@/state/modules/user';
|
||||
import ParentCard from '../components/ParentCard.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 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 parentStore = useParentStore();
|
||||
const { userInfo } = storeToRefs(userStore);
|
||||
const { activeChild } = storeToRefs(parentStore);
|
||||
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 () => {
|
||||
try {
|
||||
if (!userInfo.value.id) {
|
||||
await userStore.fetchParentUserInfo();
|
||||
}
|
||||
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||
await fetchWeeklySubject();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
|
||||
const trendText = (status: SubjectTrend['status']) => {
|
||||
const map: Record<SubjectTrend['status'], string> = {
|
||||
const trendLabel = (item: SubjectTrend) => {
|
||||
if (item.trendName) return item.trendName;
|
||||
const map: Record<NonNullable<SubjectTrend['status']>, string> = {
|
||||
up: '进步',
|
||||
down: '退步',
|
||||
stable: '稳定',
|
||||
stable: '持平',
|
||||
none: '',
|
||||
};
|
||||
return map[status];
|
||||
return item.status ? map[item.status] : '';
|
||||
};
|
||||
|
||||
const showComingSoon = (name: string) => {
|
||||
@ -230,6 +311,11 @@ const handleQuickAction = (item: QuickAction) => {
|
||||
&.up { background: #22c55e; }
|
||||
&.down { background: #f43f5e; }
|
||||
&.stable { background: #0891b2; }
|
||||
&.none { background: #94a3b8; }
|
||||
}
|
||||
|
||||
.weekly-loading {
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
|
||||
@ -42,36 +42,63 @@
|
||||
</ParentCard>
|
||||
|
||||
<template v-if="activeTab === 'analysis'">
|
||||
<ParentCard class="section-card" title="题型得分" subtitle="按题型拆解本次卷面得分">
|
||||
<view class="data-table">
|
||||
<view class="table-row table-head">
|
||||
<text>题号</text>
|
||||
<text>题型</text>
|
||||
<text>得分</text>
|
||||
<text>得分率</text>
|
||||
<ParentCard class="section-card" title="题型分布" subtitle="按题型拆解本次卷面构成">
|
||||
<view v-if="pageLoading && !analysisRows.length" class="answers-loading">
|
||||
<xy-Empty type="loading" content="加载分析数据..." />
|
||||
</view>
|
||||
<view v-for="row in detail.analysisRows" :key="row.range" class="table-row">
|
||||
<text>{{ row.range }}</text>
|
||||
<view v-else-if="analysisRows.length" class="data-table">
|
||||
<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 class="strong">{{ row.score }}</text>
|
||||
<text class="strong">{{ row.count }}</text>
|
||||
<text class="rate-good">{{ row.rate }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<xy-Empty v-else type="task" content="暂无题型分析数据" />
|
||||
</ParentCard>
|
||||
|
||||
<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">
|
||||
<text>难度</text>
|
||||
<text>题号</text>
|
||||
<text>题量</text>
|
||||
<text>得分</text>
|
||||
<text>得分率</text>
|
||||
</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.range }}</text>
|
||||
<text>{{ row.count }}</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>
|
||||
</ParentCard>
|
||||
@ -281,7 +308,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
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 ParentPage from '../components/ParentPage.vue';
|
||||
import {
|
||||
@ -332,7 +359,7 @@ const rawTitleItems = ref<Record<string, unknown>[]>([]);
|
||||
const questionSummaries = ref<ReportQuestionSummary[]>([]);
|
||||
const questionDetailCache = ref<Record<number, ReportQuestion>>({});
|
||||
const paperDetail = ref<Record<string, unknown>>({});
|
||||
const jobAnalyse = ref<Record<string, unknown>>({});
|
||||
const jobAnalyse = ref<JobAnalyseVo>({});
|
||||
|
||||
onLoad((query) => {
|
||||
recordId.value = String(query?.id || '');
|
||||
@ -346,14 +373,23 @@ onLoad((query) => {
|
||||
|
||||
const mockDetail = computed(() => reportDetails[recordId.value] || reportDetails.r1);
|
||||
|
||||
const formatRank = (rank?: unknown, total?: unknown) => {
|
||||
if (rank == null || rank === '' || total == null || total === '') return '';
|
||||
return `${rank}/${total}`;
|
||||
|
||||
const formatScoreRate = (value?: number) => {
|
||||
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 vo = (jobAnalyse.value?.jobReportVo || {}) as Record<string, unknown>;
|
||||
const mark = Number(vo.markScore ?? paperDetail.value.score ?? 0);
|
||||
const vo = jobAnalyse.value.jobReportVo || {};
|
||||
const mark = Number(vo.actualScore ?? paperDetail.value.score ?? 0);
|
||||
const total = Number(vo.totalScore ?? paperDetail.value.totalScore ?? 0);
|
||||
const rateFromScore =
|
||||
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 vo = (jobAnalyse.value?.jobReportVo || {}) as Record<string, unknown>;
|
||||
const paper = paperDetail.value;
|
||||
const classRank = formatRank(
|
||||
vo.classRank ?? paper.classRank ?? paper.classRanking,
|
||||
vo.classTotal ?? paper.classTotal ?? paper.classRankTotal,
|
||||
);
|
||||
const gradeRank = formatRank(
|
||||
vo.gradeRank ?? paper.gradeRank ?? paper.gradeRanking,
|
||||
vo.gradeTotal ?? paper.gradeTotal ?? paper.gradeRankTotal,
|
||||
);
|
||||
const rankList = classRankList.value;
|
||||
const selfRank = rankList.find((item) => item.selfFlag === 1);
|
||||
const classRank =
|
||||
selfRank?.rankNum != null && rankList.length
|
||||
? `${selfRank.rankNum}/${rankList.length}`
|
||||
: '';
|
||||
|
||||
return [
|
||||
{ label: '班级排名', value: classRank || mockDetail.value.ranks[0]?.value || '--' },
|
||||
{ label: '年级排名', value: gradeRank || mockDetail.value.ranks[1]?.value || '--' },
|
||||
{ label: '班级排名', value: classRank || '--' },
|
||||
{ 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(() => ({
|
||||
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,
|
||||
subject: reportSubject.value || String(paperDetail.value.subjectName || '多科'),
|
||||
}));
|
||||
@ -495,7 +562,7 @@ const loadReportData = async () => {
|
||||
]);
|
||||
|
||||
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;
|
||||
rawTitleItems.value = Array.isArray(titleList) ? (titleList as Record<string, unknown>[]) : [];
|
||||
@ -705,6 +772,10 @@ const goHome = () => {
|
||||
border-radius: 26rpx;
|
||||
}
|
||||
|
||||
.table-row.channel-row {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
@ -1091,6 +1162,49 @@ const goHome = () => {
|
||||
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 {
|
||||
position: fixed;
|
||||
left: 28rpx;
|
||||
|
||||
@ -11,7 +11,8 @@ export interface ParentChild {
|
||||
|
||||
export interface SubjectTrend {
|
||||
subject: string;
|
||||
status: 'up' | 'down' | 'stable';
|
||||
status?: 'up' | 'down' | 'stable' | 'none';
|
||||
trendName?: string;
|
||||
}
|
||||
|
||||
export interface GradeBand {
|
||||
|
||||
@ -170,10 +170,10 @@
|
||||
<view class="safe-bottom"></view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="home-float" @click="goHome">
|
||||
<!-- <view class="home-float" @click="goHome">
|
||||
<view class="home-roof"></view>
|
||||
<view class="home-body"></view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<view v-if="!pageLoading && filteredQuestions.length" class="bottom-bar">
|
||||
<view class="download-btn" @click="downloadWrongQuestions">一键下载错题</view>
|
||||
@ -183,9 +183,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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 { resource_type_homework } from '@/constants/doWork';
|
||||
import { useParentStore } from '@/state/modules/parent';
|
||||
import { useUserStore } from '@/state/modules/user';
|
||||
|
||||
type WrongStatus = 'new' | 'review' | 'mastered';
|
||||
type TabKey = 'all' | 'new' | 'mastered';
|
||||
@ -215,7 +219,13 @@ interface WrongQuestion {
|
||||
openPanel: 'mine' | 'analysis';
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
const parentStore = useParentStore();
|
||||
const { userInfo } = storeToRefs(userStore);
|
||||
const { activeChild } = storeToRefs(parentStore);
|
||||
|
||||
const pageLoading = ref(true);
|
||||
const summaryData = ref<WrongQuestionSummaryResult | null>(null);
|
||||
const wrongQuestions = ref<WrongQuestion[]>([]);
|
||||
const selectedSubject = ref('全部');
|
||||
const activeTab = ref<TabKey>('all');
|
||||
@ -231,21 +241,64 @@ const tabs: { key: TabKey; label: string }[] = [
|
||||
|
||||
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 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 {
|
||||
const buildSubjectStatRow = (
|
||||
name: string,
|
||||
stats: { total: number; newCount: number; reviewCount: number; masteredCount: number },
|
||||
index: number,
|
||||
) => ({
|
||||
name,
|
||||
shortName: name === '全部' ? '全' : name.slice(0, 1),
|
||||
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,
|
||||
newCount: list.filter((item) => item.status === 'new').length,
|
||||
reviewCount: list.filter((item) => item.inReviewBook).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',
|
||||
});
|
||||
|
||||
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 () => {
|
||||
pageLoading.value = true;
|
||||
try {
|
||||
await loadSummary();
|
||||
const recordsRes: any = await getListCompleteRecord();
|
||||
const records = Array.isArray(recordsRes?.data) ? recordsRes.data : Array.isArray(recordsRes) ? recordsRes : [];
|
||||
const detailRecords = await Promise.all(
|
||||
@ -421,6 +518,20 @@ const goHome = () => {
|
||||
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(() => {
|
||||
loadWrongQuestions();
|
||||
});
|
||||
|
||||
@ -104,3 +104,9 @@ export const SUBJECT_FILTER_OPTIONS: SubjectFilterOption[] = [
|
||||
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