feat:修改
This commit is contained in:
parent
5fd397b1ea
commit
43eaa23e7a
@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
|
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
|
||||||
|
import { redirectToRoleHomeIfNeeded } from '@/utils/roleHome';
|
||||||
|
|
||||||
onLaunch(() => {
|
onLaunch(() => {
|
||||||
console.log('App Launch');
|
redirectToRoleHomeIfNeeded();
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
|
|||||||
@ -1,5 +1,30 @@
|
|||||||
import { request } from '@/utils/request';
|
import { request } from '@/utils/request';
|
||||||
|
|
||||||
|
/** 试卷测试类型:1 单元测试 2 周测 3 月测 4 随堂练习 5 课前预习 */
|
||||||
|
export type PaperTestType = 1 | 2 | 3 | 4 | 5;
|
||||||
|
|
||||||
|
export const PAPER_TEST_TYPE_MAP: Record<PaperTestType, string> = {
|
||||||
|
1: '单元测试',
|
||||||
|
2: '周测',
|
||||||
|
3: '月测',
|
||||||
|
4: '随堂练习',
|
||||||
|
5: '课前预习',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PAPER_TEST_TYPE_OPTIONS: Array<{ label: string; value?: PaperTestType }> = [
|
||||||
|
{ label: '全部类型' },
|
||||||
|
{ label: '单元测试', value: 1 },
|
||||||
|
{ label: '周测', value: 2 },
|
||||||
|
{ label: '月测', value: 3 },
|
||||||
|
{ label: '随堂练习', value: 4 },
|
||||||
|
{ label: '课前预习', value: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getPaperTestTypeLabel = (type?: number | null) => {
|
||||||
|
if (!type) return '';
|
||||||
|
return PAPER_TEST_TYPE_MAP[type as PaperTestType] || '';
|
||||||
|
};
|
||||||
|
|
||||||
export interface PaperRecord {
|
export interface PaperRecord {
|
||||||
id: string;
|
id: string;
|
||||||
paperId: string;
|
paperId: string;
|
||||||
@ -11,6 +36,27 @@ export interface PaperRecord {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
remark: string;
|
remark: string;
|
||||||
costTime: number;
|
costTime: number;
|
||||||
|
totalScore?: number;
|
||||||
|
score?: number;
|
||||||
|
type?: PaperTestType;
|
||||||
|
gradeName?: string;
|
||||||
|
markTime?: string;
|
||||||
|
submitTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListCompleteRecordParams {
|
||||||
|
current?: number;
|
||||||
|
resourceType?: number;
|
||||||
|
userId?: string | number;
|
||||||
|
subjectId?: string;
|
||||||
|
type?: PaperTestType;
|
||||||
|
gradingStatus?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListCompleteRecordPage {
|
||||||
|
rows: PaperRecord[];
|
||||||
|
current?: number;
|
||||||
|
totalRows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取未完成作业列表 */
|
/** 获取未完成作业列表 */
|
||||||
@ -55,17 +101,35 @@ export const submitExam = (data: {
|
|||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 已完成作业列表 */
|
/** 已完成作业/考试记录列表 */
|
||||||
export const getListCompleteRecord = (params?: {
|
export const getListCompleteRecord = (params?: ListCompleteRecordParams) =>
|
||||||
gradingStatus?: string;
|
request<PaperRecord[] | ListCompleteRecordPage>({
|
||||||
subjectId?: string;
|
|
||||||
}) =>
|
|
||||||
request<any[]>({
|
|
||||||
url: '/school/paper/userRecord/listCompleteRecord',
|
url: '/school/paper/userRecord/listCompleteRecord',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params,
|
params: {
|
||||||
|
resourceType: 0,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 解析分页或数组两种返回结构 */
|
||||||
|
export const normalizeListCompleteRecord = (
|
||||||
|
data: PaperRecord[] | ListCompleteRecordPage | null | undefined,
|
||||||
|
) => {
|
||||||
|
if (!data) {
|
||||||
|
return { rows: [] as PaperRecord[], totalRows: 0, current: 1 };
|
||||||
|
}
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return { rows: data, totalRows: data.length, current: 1 };
|
||||||
|
}
|
||||||
|
const rows = data.rows || [];
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
totalRows: data.totalRows ?? rows.length,
|
||||||
|
current: data.current ?? 1,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/** 作业详情(含题目) */
|
/** 作业详情(含题目) */
|
||||||
export const getPaperRecordDetail = (data: { recordId: string; reportFlag: number }) =>
|
export const getPaperRecordDetail = (data: { recordId: string; reportFlag: number }) =>
|
||||||
request<any>({
|
request<any>({
|
||||||
|
|||||||
@ -75,9 +75,11 @@ export const parentSmsLogin = (data: ParentSmsLoginParams) =>
|
|||||||
data: {
|
data: {
|
||||||
bindMaOpenid: true,
|
bindMaOpenid: true,
|
||||||
adminType: 16,
|
adminType: 16,
|
||||||
openid: data.openid,
|
|
||||||
phone: data.phone,
|
phone: data.phone,
|
||||||
verifyCode: data.verifyCode,
|
verifyCode: data.verifyCode,
|
||||||
|
wxUser: {
|
||||||
|
openid: data.openid,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -89,6 +91,24 @@ export const logout = () =>
|
|||||||
silent: true,
|
silent: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface SysUserInfo {
|
||||||
|
id?: string | number;
|
||||||
|
account?: string;
|
||||||
|
phone?: string;
|
||||||
|
nickName?: string;
|
||||||
|
name?: string;
|
||||||
|
avatar?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取当前登录用户信息(家长端登录后使用) */
|
||||||
|
export const selectUser = () =>
|
||||||
|
request<SysUserInfo>({
|
||||||
|
url: '/sysUser/selectUser',
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
/** 获取学生用户首页信息 */
|
/** 获取学生用户首页信息 */
|
||||||
export const getUserInfo = () =>
|
export const getUserInfo = () =>
|
||||||
request<any>({
|
request<any>({
|
||||||
|
|||||||
@ -40,13 +40,71 @@ export interface PageResult<T> {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 家长绑定孩子列表 */
|
/** 家长绑定孩子列表(当前家长已绑定) */
|
||||||
export const getParentBindChildList = (params?: { current?: number; size?: number }) =>
|
export const getParentBindChildList = (params: {
|
||||||
|
parentId: string | number;
|
||||||
|
current?: number;
|
||||||
|
size?: number;
|
||||||
|
}) =>
|
||||||
request<PageResult<ParentBindChildItem>>({
|
request<PageResult<ParentBindChildItem>>({
|
||||||
url: '/parentBindChild/list',
|
url: '/parentBindChild/list',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: {
|
params: {
|
||||||
current: params?.current ?? 1,
|
parentId: params.parentId,
|
||||||
size: params?.size ?? 100,
|
current: params.current ?? 1,
|
||||||
|
size: params.size ?? 100,
|
||||||
|
bindFlag: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 按孩子 ID 查询绑定家长(扫码绑定前校验) */
|
||||||
|
export const getBindParentsByChildId = (childId: string | number) =>
|
||||||
|
request<PageResult<ParentBindChildItem>>({
|
||||||
|
url: '/parentBindChild/list',
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
childId,
|
||||||
|
size: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 按设备序列号查询绑定家长 */
|
||||||
|
export const getBindParentsByDevice = (simSerialNumber: string) =>
|
||||||
|
request<PageResult<any>>({
|
||||||
|
url: '/parentBindDevice/list',
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
simSerialNumber,
|
||||||
|
size: -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 手动输入孩子账号绑定 */
|
||||||
|
export const manualBindChild = (childAcc: string) =>
|
||||||
|
request<number>({
|
||||||
|
url: '/parentBind/familyTeacherBindAcc',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
data: `childAcc=${encodeURIComponent(childAcc.trim())}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 扫码后申请绑定孩子/设备 */
|
||||||
|
export const parentBindApply = (params: {
|
||||||
|
childId?: string | number;
|
||||||
|
simSerialNumber?: string;
|
||||||
|
identityType: number;
|
||||||
|
}) => {
|
||||||
|
const childDto = params.childId
|
||||||
|
? { childId: params.childId, identityType: params.identityType }
|
||||||
|
: undefined;
|
||||||
|
const deviceDto = params.simSerialNumber
|
||||||
|
? { simSerialNumber: params.simSerialNumber, identityType: params.identityType }
|
||||||
|
: undefined;
|
||||||
|
return request({
|
||||||
|
url: '/parentBind/apply',
|
||||||
|
method: 'POST',
|
||||||
|
data: { childDto, deviceDto },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@ -1,30 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="empty">
|
<view class="empty">
|
||||||
<view class="empty-illu" :class="`empty-illu-${type}`">
|
<view class="empty-illu" :class="`empty-illu-${type}`">
|
||||||
<!-- Loading -->
|
|
||||||
<view v-if="type === 'loading'" class="empty-loader">
|
<view v-if="type === 'loading'" class="empty-loader">
|
||||||
<view class="dot dot-1"></view>
|
<view class="dot dot-1"></view>
|
||||||
<view class="dot dot-2"></view>
|
<view class="dot dot-2"></view>
|
||||||
<view class="dot dot-3"></view>
|
<view class="dot dot-3"></view>
|
||||||
</view>
|
</view>
|
||||||
<!-- Network -->
|
<view v-else class="empty-art">
|
||||||
<svg v-else-if="type === 'network'" width="80" height="80" viewBox="0 0 24 24" fill="none">
|
<view class="art-card art-card-back"></view>
|
||||||
<path d="M5 13a10 10 0 0 1 14 0M8.5 16.5a5 5 0 0 1 7 0M12 20h0M2 8.82a15 15 0 0 1 20 0"
|
<view class="art-card art-card-front">
|
||||||
stroke="#A78BFA" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
<view class="art-dot"></view>
|
||||||
<line x1="3" y1="3" x2="21" y2="21" stroke="#EF4444" stroke-width="1.6" stroke-linecap="round" />
|
<view class="art-line art-line-long"></view>
|
||||||
</svg>
|
<view class="art-line art-line-mid"></view>
|
||||||
<!-- Task / 默认空盒子 -->
|
<view class="art-line art-line-short"></view>
|
||||||
<svg v-else-if="type === 'task'" width="80" height="80" viewBox="0 0 24 24" fill="none">
|
</view>
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
<view class="art-spark art-spark-left"></view>
|
||||||
stroke="#A78BFA" stroke-width="1.6" stroke-linejoin="round" />
|
<view class="art-spark art-spark-right"></view>
|
||||||
<path d="M14 2v6h6" stroke="#A78BFA" stroke-width="1.6" stroke-linejoin="round" />
|
</view>
|
||||||
<path d="M9 13h6M9 17h4" stroke="#A78BFA" stroke-width="1.6" stroke-linecap="round" />
|
|
||||||
</svg>
|
|
||||||
<!-- Learn -->
|
|
||||||
<svg v-else width="80" height="80" viewBox="0 0 24 24" fill="none">
|
|
||||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"
|
|
||||||
stroke="#A78BFA" stroke-width="1.6" stroke-linejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="empty-text">{{ content }}</view>
|
<view class="empty-text">{{ content }}</view>
|
||||||
</view>
|
</view>
|
||||||
@ -68,9 +60,16 @@ withDefaults(
|
|||||||
inset 0 -6rpx 0 rgba(199, 210, 254, 0.6),
|
inset 0 -6rpx 0 rgba(199, 210, 254, 0.6),
|
||||||
inset 0 4rpx 0 rgba(255, 255, 255, 0.9);
|
inset 0 4rpx 0 rgba(255, 255, 255, 0.9);
|
||||||
|
|
||||||
svg {
|
&.empty-illu-network {
|
||||||
width: 90rpx;
|
.art-dot {
|
||||||
height: 90rpx;
|
background: #fca5a5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.empty-illu-learn {
|
||||||
|
.art-dot {
|
||||||
|
background: #86efac;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +79,82 @@ withDefaults(
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-art {
|
||||||
|
position: relative;
|
||||||
|
width: 116rpx;
|
||||||
|
height: 116rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-card {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-card-back {
|
||||||
|
right: 6rpx;
|
||||||
|
bottom: 4rpx;
|
||||||
|
width: 78rpx;
|
||||||
|
height: 86rpx;
|
||||||
|
background: linear-gradient(180deg, #dbeafe 0%, #c7d2fe 100%);
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: rotate(9deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-card-front {
|
||||||
|
left: 8rpx;
|
||||||
|
top: 6rpx;
|
||||||
|
width: 86rpx;
|
||||||
|
height: 98rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 10rpx 24rpx rgba(79, 70, 229, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-dot {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
margin: 20rpx 0 0 18rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-line {
|
||||||
|
height: 8rpx;
|
||||||
|
margin: 12rpx 0 0 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #c7d2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-line-long {
|
||||||
|
width: 50rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-line-mid {
|
||||||
|
width: 38rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-line-short {
|
||||||
|
width: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-spark {
|
||||||
|
position: absolute;
|
||||||
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #a5f3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-spark-left {
|
||||||
|
left: 0;
|
||||||
|
bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.art-spark-right {
|
||||||
|
right: 0;
|
||||||
|
top: 18rpx;
|
||||||
|
background: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
/* loading dots */
|
/* loading dots */
|
||||||
.empty-loader {
|
.empty-loader {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -256,6 +256,20 @@
|
|||||||
"pageOrientation": "portrait"
|
"pageOrientation": "portrait"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bind/index",
|
||||||
|
"name": "parent-bind",
|
||||||
|
"meta": {
|
||||||
|
"islogin": true,
|
||||||
|
"role": "parent"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "绑定孩子",
|
||||||
|
"mp-weixin": {
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -143,7 +143,7 @@ import { onShow } from '@dcloudio/uni-app';
|
|||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useUserStore } from '@/state/modules/user';
|
import { useUserStore } from '@/state/modules/user';
|
||||||
import { getUnfinishedHomework } from '@/api/homework';
|
import { getUnfinishedHomework } from '@/api/homework';
|
||||||
import { getCache } from '@/utils/cache';
|
import { redirectToRoleHomeIfNeeded } from '@/utils/roleHome';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { userInfo, token, isLogin } = storeToRefs(userStore);
|
const { userInfo, token, isLogin } = storeToRefs(userStore);
|
||||||
@ -216,10 +216,7 @@ const fetchUnfinished = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (token.value && getCache('role') === 'teacher') {
|
if (redirectToRoleHomeIfNeeded()) return;
|
||||||
uni.reLaunch({ url: '/pages/teacher/index/index' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (token.value) {
|
if (token.value) {
|
||||||
userStore.fetchUserInfo().catch(() => { /* ignore */ });
|
userStore.fetchUserInfo().catch(() => { /* ignore */ });
|
||||||
fetchUnfinished();
|
fetchUnfinished();
|
||||||
|
|||||||
@ -184,6 +184,7 @@ import { parentMpLogin, sendParentLoginCode } from '@/api/login';
|
|||||||
import { useUserStore } from '@/state/modules/user';
|
import { useUserStore } from '@/state/modules/user';
|
||||||
import { useTeacherStore } from '@/state/modules/teacher';
|
import { useTeacherStore } from '@/state/modules/teacher';
|
||||||
import { setCache, getCache } from '@/utils/cache';
|
import { setCache, getCache } from '@/utils/cache';
|
||||||
|
import { redirectToRoleHomeIfNeeded } from '@/utils/roleHome';
|
||||||
|
|
||||||
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
|
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
|
||||||
const logoUrl = `${OSS_URL}/urm/logo.png`;
|
const logoUrl = `${OSS_URL}/urm/logo.png`;
|
||||||
@ -282,6 +283,7 @@ onLoad((options: any) => {
|
|||||||
if (options?.a) {
|
if (options?.a) {
|
||||||
account.value = options.a;
|
account.value = options.a;
|
||||||
}
|
}
|
||||||
|
if (redirectToRoleHomeIfNeeded()) return;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onLogoTap = () => {
|
const onLogoTap = () => {
|
||||||
@ -362,6 +364,7 @@ const onParentAuthorize = async () => {
|
|||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
await userStore.parentMpLoginSuccess(accessToken);
|
await userStore.parentMpLoginSuccess(accessToken);
|
||||||
|
setCache('role', 'parent');
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
uni.showToast({ title: '登录成功', icon: 'success' });
|
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||||
setTimeout(() => navigateAfterLogin(), 800);
|
setTimeout(() => navigateAfterLogin(), 800);
|
||||||
@ -435,6 +438,7 @@ const onLogin = async () => {
|
|||||||
verifyCode: code,
|
verifyCode: code,
|
||||||
openid: parentWxOpenid.value,
|
openid: parentWxOpenid.value,
|
||||||
});
|
});
|
||||||
|
setCache('role', 'parent');
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
uni.showToast({ title: '登录成功', icon: 'success' });
|
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||||
setTimeout(() => navigateAfterLogin(), 800);
|
setTimeout(() => navigateAfterLogin(), 800);
|
||||||
|
|||||||
@ -1,25 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<ParentPage active="reports" :show-tab="false" :use-scroll-view="false">
|
<ParentPage active="reports" :show-tab="false" :use-scroll-view="false">
|
||||||
<view class="filter-row">
|
<ParentFilterRow :items="filterItems" @change="onFilterChange" />
|
||||||
<picker class="filter-picker" :range="subjectOptions" :value="subjectIndex" @change="onSubjectChange">
|
|
||||||
<view class="filter-pill">
|
|
||||||
<text>{{ subjectOptions[subjectIndex] }}</text>
|
|
||||||
<view class="filter-arrow"></view>
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
<picker class="filter-picker" :range="gradeOptions" :value="gradeIndex" @change="onGradeChange">
|
|
||||||
<view class="filter-pill">
|
|
||||||
<text>{{ gradeOptions[gradeIndex] }}</text>
|
|
||||||
<view class="filter-arrow"></view>
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
<picker class="filter-picker" :range="categoryOptions" :value="categoryIndex" @change="onCategoryChange">
|
|
||||||
<view class="filter-pill">
|
|
||||||
<text>{{ categoryOptions[categoryIndex] }}</text>
|
|
||||||
<view class="filter-arrow"></view>
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="module-section">
|
<view class="module-section">
|
||||||
<ParentCard title="错题知识点占比" subtitle="按错题占比识别最近需要优先补强的知识点">
|
<ParentCard title="错题知识点占比" subtitle="按错题占比识别最近需要优先补强的知识点">
|
||||||
@ -113,6 +94,7 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onReady, onShow } from '@dcloudio/uni-app';
|
import { onReady, onShow } from '@dcloudio/uni-app';
|
||||||
import ParentCard from '../components/ParentCard.vue';
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
|
import ParentFilterRow from '../components/ParentFilterRow.vue';
|
||||||
import ParentPage from '../components/ParentPage.vue';
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
import {
|
import {
|
||||||
abilitySummary,
|
abilitySummary,
|
||||||
@ -131,6 +113,26 @@ const gradeIndex = ref(1);
|
|||||||
const categoryIndex = ref(0);
|
const categoryIndex = ref(0);
|
||||||
const activeLevel = ref<ScoreLevel>('weak');
|
const activeLevel = ref<ScoreLevel>('weak');
|
||||||
|
|
||||||
|
const filterItems = computed(() => [
|
||||||
|
{ key: 'subject', range: subjectOptions, value: subjectIndex.value },
|
||||||
|
{ key: 'grade', range: gradeOptions, value: gradeIndex.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 scoreLevels: { key: ScoreLevel; label: string }[] = [
|
const scoreLevels: { key: ScoreLevel; label: string }[] = [
|
||||||
{ key: 'weak', label: '薄弱' },
|
{ key: 'weak', label: '薄弱' },
|
||||||
{ key: 'pass', label: '合格' },
|
{ key: 'pass', label: '合格' },
|
||||||
@ -237,48 +239,9 @@ const abilityChartOpts = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubjectChange = (e: any) => { subjectIndex.value = Number(e.detail.value); };
|
|
||||||
const onGradeChange = (e: any) => { gradeIndex.value = Number(e.detail.value); };
|
|
||||||
const onCategoryChange = (e: any) => { categoryIndex.value = Number(e.detail.value); };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.filter-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 16rpx;
|
|
||||||
padding-top: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-picker {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-pill {
|
|
||||||
height: 72rpx;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10rpx;
|
|
||||||
color: #164e63;
|
|
||||||
font-size: 24rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
background: rgba(255, 255, 255, 0.94);
|
|
||||||
box-shadow:
|
|
||||||
0 8rpx 22rpx rgba(8, 145, 178, 0.08),
|
|
||||||
inset 0 -2rpx 0 rgba(207, 250, 254, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-arrow {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 8rpx solid transparent;
|
|
||||||
border-right: 8rpx solid transparent;
|
|
||||||
border-top: 10rpx solid #0891b2;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-section {
|
.module-section {
|
||||||
margin-top: 44rpx;
|
margin-top: 44rpx;
|
||||||
}
|
}
|
||||||
|
|||||||
375
src/pages/parent/bind/index.vue
Normal file
375
src/pages/parent/bind/index.vue
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
<template>
|
||||||
|
<view class="bind-page">
|
||||||
|
<view v-if="loading" class="state-wrap">
|
||||||
|
<text class="state-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<template v-else>
|
||||||
|
<view v-if="simSerialNumber" class="section">
|
||||||
|
<view class="section-title">
|
||||||
|
设备
|
||||||
|
<text v-if="hasBindDevice" class="tag-bound">(已绑定)</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-card">
|
||||||
|
<text class="info-main">{{ deviceDisplay }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="childId" class="section">
|
||||||
|
<view class="section-title">
|
||||||
|
孩子
|
||||||
|
<text v-if="hasBindChild" class="tag-bound">(已绑定)</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-card child-card">
|
||||||
|
<image
|
||||||
|
v-if="childAvatar"
|
||||||
|
class="child-avatar"
|
||||||
|
:src="childAvatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="child-meta">
|
||||||
|
<text class="info-main">{{ childDisplayName }}</text>
|
||||||
|
<text v-if="childAccountText" class="info-sub">账号:{{ childAccountText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">请选择您的身份</view>
|
||||||
|
<view class="identity-grid">
|
||||||
|
<view
|
||||||
|
v-for="item in PARENT_IDENTITY_LIST"
|
||||||
|
:key="item.id"
|
||||||
|
class="identity-item"
|
||||||
|
:class="{ active: identityType === item.id, disabled: !canBind }"
|
||||||
|
@click="selectIdentity(item.id)"
|
||||||
|
>
|
||||||
|
<image class="identity-icon" :src="item.icon" mode="aspectFit" />
|
||||||
|
<text class="identity-name">{{ item.name }}</text>
|
||||||
|
<view v-if="identityType === item.id" class="identity-check">✓</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="footer">
|
||||||
|
<view
|
||||||
|
class="submit-btn"
|
||||||
|
:class="{ disabled: !canSubmit || hasAllBound }"
|
||||||
|
@click="submitBind"
|
||||||
|
>
|
||||||
|
{{ hasAllBound ? '已绑定' : submitting ? '提交中...' : '申请绑定' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import {
|
||||||
|
getBindParentsByChildId,
|
||||||
|
getBindParentsByDevice,
|
||||||
|
parentBindApply,
|
||||||
|
type ParentBindChildItem,
|
||||||
|
} from '@/api/parent';
|
||||||
|
import { useUserStore } from '@/state/modules/user';
|
||||||
|
import {
|
||||||
|
PARENT_IDENTITY_LIST,
|
||||||
|
PARENT_TYPE_OTHER,
|
||||||
|
} from '../utils/parentIdentity';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { userInfo } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const childId = ref('');
|
||||||
|
const childAccount = ref('');
|
||||||
|
const simSerialNumber = ref('');
|
||||||
|
const identityType = ref(PARENT_TYPE_OTHER);
|
||||||
|
|
||||||
|
const bindChildRow = ref<ParentBindChildItem | null>(null);
|
||||||
|
const bindDeviceRow = ref<any | null>(null);
|
||||||
|
const hasBindChild = ref(false);
|
||||||
|
const hasBindDevice = ref(false);
|
||||||
|
|
||||||
|
const childDisplayName = computed(() => {
|
||||||
|
if (bindChildRow.value?.child?.name || bindChildRow.value?.child?.nickname) {
|
||||||
|
return bindChildRow.value.child.name || bindChildRow.value.child.nickname || '孩子';
|
||||||
|
}
|
||||||
|
return childAccount.value || '孩子';
|
||||||
|
});
|
||||||
|
|
||||||
|
const childAccountText = computed(() => bindChildRow.value?.child?.account || childAccount.value || '');
|
||||||
|
const childAvatar = computed(() => bindChildRow.value?.child?.avatar || '');
|
||||||
|
|
||||||
|
const deviceDisplay = computed(() => {
|
||||||
|
if (bindDeviceRow.value?.simSerialNumber) return bindDeviceRow.value.simSerialNumber;
|
||||||
|
return simSerialNumber.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const needBoth = computed(() => !!(childId.value && simSerialNumber.value));
|
||||||
|
|
||||||
|
const hasAllBound = computed(() => {
|
||||||
|
if (needBoth.value) return hasBindChild.value && hasBindDevice.value;
|
||||||
|
if (simSerialNumber.value) return hasBindDevice.value;
|
||||||
|
return hasBindChild.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const bindChildIdToApply = computed(() => {
|
||||||
|
if (!childId.value || hasBindChild.value) return undefined;
|
||||||
|
return childId.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const bindDeviceSnToApply = computed(() => {
|
||||||
|
if (!simSerialNumber.value || hasBindDevice.value) return undefined;
|
||||||
|
return simSerialNumber.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canBind = computed(() => !!(bindChildIdToApply.value || bindDeviceSnToApply.value));
|
||||||
|
|
||||||
|
const canSubmit = computed(() => canBind.value && !submitting.value);
|
||||||
|
|
||||||
|
const checkBindStatus = async () => {
|
||||||
|
const parentId = userInfo.value.id;
|
||||||
|
if (!parentId) {
|
||||||
|
await userStore.fetchParentUserInfo().catch(() => { /* ignore */ });
|
||||||
|
}
|
||||||
|
const currentParentId = userInfo.value.id;
|
||||||
|
|
||||||
|
if (childId.value) {
|
||||||
|
const res = await getBindParentsByChildId(childId.value);
|
||||||
|
const rows = res.data?.rows || [];
|
||||||
|
if (rows.length) {
|
||||||
|
bindChildRow.value = rows[0];
|
||||||
|
}
|
||||||
|
hasBindChild.value = rows.some(
|
||||||
|
(item) => String(item.parent?.id) === String(currentParentId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (simSerialNumber.value) {
|
||||||
|
const res = await getBindParentsByDevice(simSerialNumber.value);
|
||||||
|
const rows = res.data?.rows || [];
|
||||||
|
if (rows.length) {
|
||||||
|
bindDeviceRow.value = rows[0];
|
||||||
|
}
|
||||||
|
hasBindDevice.value = rows.some(
|
||||||
|
(item) => String(item.parent?.id) === String(currentParentId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad(async (query) => {
|
||||||
|
childId.value = query?.childId ? String(query.childId) : '';
|
||||||
|
childAccount.value = query?.account ? decodeURIComponent(String(query.account)) : '';
|
||||||
|
simSerialNumber.value = query?.simSerialNumber
|
||||||
|
? decodeURIComponent(String(query.simSerialNumber))
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (!childId.value && !simSerialNumber.value) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '参数错误',
|
||||||
|
showCancel: false,
|
||||||
|
success: () => uni.navigateBack(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await checkBindStatus();
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({ title: e?.message || '加载失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectIdentity = (id: number) => {
|
||||||
|
if (!canBind.value) return;
|
||||||
|
identityType.value = id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitBind = async () => {
|
||||||
|
if (hasAllBound.value || !canSubmit.value) return;
|
||||||
|
if (!identityType.value) {
|
||||||
|
uni.showToast({ title: '请选择您与孩子的关系', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await parentBindApply({
|
||||||
|
childId: bindChildIdToApply.value,
|
||||||
|
simSerialNumber: bindDeviceSnToApply.value,
|
||||||
|
identityType: identityType.value,
|
||||||
|
});
|
||||||
|
uni.showToast({ title: '已发出绑定申请', icon: 'success' });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack();
|
||||||
|
}, 1200);
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({ title: e?.message || '申请失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bind-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 180rpx;
|
||||||
|
background: linear-gradient(180deg, #ecfeff 0%, #f8fafc 40%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-wrap {
|
||||||
|
padding: 120rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-text {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-bound {
|
||||||
|
color: #ef4444;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
padding: 28rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
box-shadow: 0 12rpx 32rpx rgba(8, 145, 178, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-avatar {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #ecfeff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-main {
|
||||||
|
display: block;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-sub {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 24rpx 20rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
border: 2rpx solid transparent;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(8, 145, 178, 0.06);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #0891b2;
|
||||||
|
background: #ecfeff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity-icon {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity-name {
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identity-check {
|
||||||
|
position: absolute;
|
||||||
|
top: 16rpx;
|
||||||
|
right: 16rpx;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0891b2;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
background: linear-gradient(135deg, #0891b2, #06b6d4);
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
157
src/pages/parent/components/ManualBindChild.vue
Normal file
157
src/pages/parent/components/ManualBindChild.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="visible" class="manual-bind-mask" @click="close">
|
||||||
|
<view class="manual-bind-panel" @click.stop>
|
||||||
|
<text class="panel-title">手动绑定孩子账号</text>
|
||||||
|
<input
|
||||||
|
v-model="childAccount"
|
||||||
|
class="panel-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入孩子账号"
|
||||||
|
placeholder-class="panel-placeholder"
|
||||||
|
:adjust-position="false"
|
||||||
|
@confirm="confirm"
|
||||||
|
/>
|
||||||
|
<view class="panel-actions">
|
||||||
|
<view class="panel-btn cancel" @click="close">取消</view>
|
||||||
|
<view class="panel-btn primary" :class="{ disabled: submitting }" @click="confirm">
|
||||||
|
{{ submitting ? '绑定中...' : '绑定' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { manualBindChild } from '@/api/parent';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', value: boolean): void;
|
||||||
|
(e: 'success'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const childAccount = ref('');
|
||||||
|
const submitting = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
if (!val) childAccount.value = '';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (submitting.value) return;
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
const acc = childAccount.value.trim();
|
||||||
|
if (!acc) {
|
||||||
|
uni.showToast({ title: '请输入孩子账号', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (submitting.value) return;
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
const res = await manualBindChild(acc);
|
||||||
|
const code = Number(res.data);
|
||||||
|
if (code === 1) {
|
||||||
|
uni.showToast({ title: '绑定成功', icon: 'success' });
|
||||||
|
emit('success');
|
||||||
|
emit('update:visible', false);
|
||||||
|
} else if (code === 2) {
|
||||||
|
uni.showToast({ title: '已向管理员发送绑定请求', icon: 'none' });
|
||||||
|
emit('success');
|
||||||
|
emit('update:visible', false);
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '绑定失败', icon: 'none' });
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({ title: e?.message || '绑定失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.manual-bind-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(15, 23, 42, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-bind-panel {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 620rpx;
|
||||||
|
padding: 40rpx 36rpx 32rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 24rpx 60rpx rgba(8, 145, 178, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
display: block;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-input {
|
||||||
|
height: 88rpx;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
border: 2rpx solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-top: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
color: #64748b;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #0891b2, #06b6d4);
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
85
src/pages/parent/components/ParentFilterRow.vue
Normal file
85
src/pages/parent/components/ParentFilterRow.vue
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<view class="filter-row" :style="gridStyle">
|
||||||
|
<picker
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.key"
|
||||||
|
class="filter-picker"
|
||||||
|
:range="item.range"
|
||||||
|
:value="item.value"
|
||||||
|
@change="(e) => onPickerChange(item.key, e)"
|
||||||
|
>
|
||||||
|
<view class="filter-pill">
|
||||||
|
<text class="filter-text">{{ item.range[item.value] }}</text>
|
||||||
|
<view class="filter-arrow"></view>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
export interface ParentFilterItem {
|
||||||
|
key: string;
|
||||||
|
range: string[];
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
items: ParentFilterItem[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
change: [payload: { key: string; value: number }];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const gridStyle = computed(() => ({
|
||||||
|
gridTemplateColumns: `repeat(${Math.max(props.items?.length || 1, 1)}, 1fr)`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onPickerChange = (key: string, e: { detail: { value: string } }) => {
|
||||||
|
emit('change', { key, value: Number(e.detail.value) });
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.filter-row {
|
||||||
|
display: grid;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-picker {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill {
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
color: #164e63;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow:
|
||||||
|
0 8rpx 22rpx rgba(8, 145, 178, 0.08),
|
||||||
|
inset 0 -2rpx 0 rgba(207, 250, 254, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-text {
|
||||||
|
max-width: 260rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8rpx solid transparent;
|
||||||
|
border-right: 8rpx solid transparent;
|
||||||
|
border-top: 10rpx solid #0891b2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -2,7 +2,15 @@
|
|||||||
<view class="parent-page" :class="{ 'native-scroll': !useScrollView }">
|
<view class="parent-page" :class="{ 'native-scroll': !useScrollView }">
|
||||||
<view class="decor decor-a"></view>
|
<view class="decor decor-a"></view>
|
||||||
<view class="decor decor-b"></view>
|
<view class="decor decor-b"></view>
|
||||||
<scroll-view v-if="useScrollView" class="page-scroll" scroll-y enhanced :show-scrollbar="false">
|
<scroll-view
|
||||||
|
v-if="useScrollView"
|
||||||
|
class="page-scroll"
|
||||||
|
scroll-y
|
||||||
|
enhanced
|
||||||
|
:show-scrollbar="false"
|
||||||
|
:lower-threshold="120"
|
||||||
|
@scrolltolower="$emit('scrolltolower')"
|
||||||
|
>
|
||||||
<view class="page-content" :class="{ 'with-tab': showTab }">
|
<view class="page-content" :class="{ 'with-tab': showTab }">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</view>
|
</view>
|
||||||
@ -19,6 +27,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ParentTabBar from './ParentTabBar.vue';
|
import ParentTabBar from './ParentTabBar.vue';
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
scrolltolower: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
// 家长端页面统一背景、滚动区域和底部导航,业务页只关注内容块。
|
// 家长端页面统一背景、滚动区域和底部导航,业务页只关注内容块。
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<ParentPage active="home">
|
<ParentPage active="home">
|
||||||
<view class="profile">
|
<view class="profile">
|
||||||
<view class="avatar">{{ activeChild.name.slice(0, 1) }}</view>
|
<image class="avatar" :src="parentAvatarUrl" mode="aspectFill" />
|
||||||
<view class="profile-main">
|
<view class="profile-main">
|
||||||
<view class="name-row">
|
<view class="name-row">
|
||||||
<text class="child-name">{{ activeChild.name }}家长</text>
|
<text class="child-name">{{ activeChild.name }}家长</text>
|
||||||
@ -58,11 +58,33 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useParentStore } from '@/state/modules/parent';
|
||||||
|
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, parentChildren, quickActions, type QuickAction, type SubjectTrend } from '../utils/data';
|
import { gradeBands, quickActions, type QuickAction, type SubjectTrend } from '../utils/data';
|
||||||
|
|
||||||
const activeChild = parentChildren[0];
|
const DEFAULT_AVATAR = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/logo.png';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const parentStore = useParentStore();
|
||||||
|
const { userInfo } = storeToRefs(userStore);
|
||||||
|
const { activeChild } = storeToRefs(parentStore);
|
||||||
|
const parentAvatarUrl = computed(() => userInfo.value.avatarUrl || DEFAULT_AVATAR);
|
||||||
|
|
||||||
|
onShow(async () => {
|
||||||
|
try {
|
||||||
|
if (!userInfo.value.id) {
|
||||||
|
await userStore.fetchParentUserInfo();
|
||||||
|
}
|
||||||
|
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const trendText = (status: SubjectTrend['status']) => {
|
const trendText = (status: SubjectTrend['status']) => {
|
||||||
const map: Record<SubjectTrend['status'], string> = {
|
const map: Record<SubjectTrend['status'], string> = {
|
||||||
@ -105,13 +127,8 @@ const handleQuickAction = (item: QuickAction) => {
|
|||||||
width: 108rpx;
|
width: 108rpx;
|
||||||
height: 108rpx;
|
height: 108rpx;
|
||||||
border-radius: 36rpx;
|
border-radius: 36rpx;
|
||||||
display: flex;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
background: #ecfeff;
|
||||||
justify-content: center;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 900;
|
|
||||||
background: linear-gradient(180deg, #22d3ee 0%, #0891b2 100%);
|
|
||||||
box-shadow: 0 12rpx 28rpx rgba(8, 145, 178, 0.24);
|
box-shadow: 0 12rpx 28rpx rgba(8, 145, 178, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
<view class="account-card">
|
<view class="account-card">
|
||||||
<ParentCard>
|
<ParentCard>
|
||||||
<view class="account-row">
|
<view class="account-row">
|
||||||
<view class="account-avatar">家</view>
|
<image class="account-avatar" :src="parentAvatarUrl" mode="aspectFill" />
|
||||||
<view class="account-main">
|
<view class="account-main">
|
||||||
<text class="phone">{{ displayPhone }}</text>
|
<text class="nickname">{{ displayNickname }}</text>
|
||||||
<text class="current-child">当前孩子:{{ activeChild.name }} · {{ activeChild.className || '暂无年级' }}</text>
|
<text class="current-child">当前孩子:{{ activeChild.name }} · {{ activeChild.className || '暂无年级' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -28,10 +28,19 @@
|
|||||||
<text class="child-account">{{ child.account }}</text>
|
<text class="child-account">{{ child.account }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="bind-btn" @click="bindChild">+ 绑定孩子账号</view>
|
<view class="bind-actions">
|
||||||
|
<view class="bind-action-btn" @click="openManualBind">手动绑定</view>
|
||||||
|
<view class="bind-action-btn primary" @click="onScanBind">扫码绑定</view>
|
||||||
|
</view>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<ManualBindChild
|
||||||
|
:visible="manualBindVisible"
|
||||||
|
@update:visible="manualBindVisible = $event"
|
||||||
|
@success="onBindSuccess"
|
||||||
|
/>
|
||||||
|
|
||||||
<view class="menu-card">
|
<view class="menu-card">
|
||||||
<ParentCard>
|
<ParentCard>
|
||||||
<view class="menu-item" @click="showAbout">
|
<view class="menu-item" @click="showAbout">
|
||||||
@ -50,85 +59,71 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
import { getParentBindChildList, type ParentBindChildItem } from '@/api/parent';
|
import { storeToRefs } from 'pinia';
|
||||||
|
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 ManualBindChild from '../components/ManualBindChild.vue';
|
||||||
|
import { scanAndGoBind } from '../utils/scanBind';
|
||||||
|
|
||||||
interface ChildItem {
|
const DEFAULT_AVATAR = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/logo.png';
|
||||||
bindId: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
account: string;
|
|
||||||
className: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const loading = ref(false);
|
const parentStore = useParentStore();
|
||||||
|
const { userInfo } = storeToRefs(userStore);
|
||||||
|
const { childrenList, activeChild, activeChildIndex, loading } = storeToRefs(parentStore);
|
||||||
|
const parentAvatarUrl = computed(() => userInfo.value.avatarUrl || DEFAULT_AVATAR);
|
||||||
const parentPhone = ref('');
|
const parentPhone = ref('');
|
||||||
const childrenList = ref<ChildItem[]>([]);
|
const manualBindVisible = ref(false);
|
||||||
const activeChildIndex = ref(0);
|
|
||||||
|
|
||||||
const activeChild = computed(() => childrenList.value[activeChildIndex.value] || {
|
const displayNickname = computed(() => {
|
||||||
name: '--',
|
const nick = userInfo.value.nickName;
|
||||||
className: '--',
|
if (nick != null && String(nick).trim() !== '') {
|
||||||
account: '',
|
return String(nick).trim();
|
||||||
id: '',
|
|
||||||
bindId: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const displayPhone = computed(() => maskPhone(parentPhone.value));
|
|
||||||
|
|
||||||
const maskPhone = (phone?: string) => {
|
|
||||||
const value = (phone || '').trim();
|
|
||||||
if (!value) return '--';
|
|
||||||
if (value.length < 7) return value;
|
|
||||||
return `${value.slice(0, 3)}****${value.slice(-4)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapBindRow = (row: ParentBindChildItem): ChildItem => ({
|
|
||||||
bindId: String(row.id ?? row.child?.id ?? ''),
|
|
||||||
id: String(row.child?.id ?? row.id ?? ''),
|
|
||||||
name: row.child?.name || row.child?.nickname || '未命名',
|
|
||||||
account: row.child?.account || '',
|
|
||||||
className: row.child?.gradeName || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchChildren = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const res = await getParentBindChildList();
|
|
||||||
const rows = res.data?.rows || [];
|
|
||||||
childrenList.value = rows.map(mapBindRow);
|
|
||||||
parentPhone.value = rows.find((item) => item.parent?.phone)?.parent?.phone || '';
|
|
||||||
if (activeChildIndex.value >= childrenList.value.length) {
|
|
||||||
activeChildIndex.value = 0;
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
childrenList.value = [];
|
|
||||||
uni.showToast({ title: e?.message || '获取孩子列表失败', icon: 'none' });
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
const phone = userInfo.value.phone || parentPhone.value;
|
||||||
|
return phone?.trim() || '--';
|
||||||
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(async () => {
|
||||||
fetchChildren();
|
parentPhone.value = userInfo.value.phone || parentPhone.value;
|
||||||
|
if (childrenList.value.length) return;
|
||||||
|
try {
|
||||||
|
if (!userInfo.value.id) {
|
||||||
|
await userStore.fetchParentUserInfo();
|
||||||
|
}
|
||||||
|
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectChild = (index: number) => {
|
const selectChild = (index: number) => {
|
||||||
activeChildIndex.value = index;
|
parentStore.selectChildByIndex(index);
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: `已切换到${childrenList.value[index]?.name || '孩子'}`,
|
title: `已切换到${childrenList.value[index]?.name || '孩子'}`,
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindChild = () => {
|
const openManualBind = () => {
|
||||||
uni.showToast({
|
manualBindVisible.value = true;
|
||||||
title: '绑定功能建设中',
|
};
|
||||||
icon: 'none',
|
|
||||||
});
|
const onScanBind = () => {
|
||||||
|
scanAndGoBind();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBindSuccess = async () => {
|
||||||
|
try {
|
||||||
|
if (!userInfo.value.id) {
|
||||||
|
await userStore.fetchParentUserInfo();
|
||||||
|
}
|
||||||
|
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({ title: e?.message || '刷新孩子列表失败', icon: 'none' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showAbout = () => {
|
const showAbout = () => {
|
||||||
@ -166,13 +161,8 @@ const handleLogout = () => {
|
|||||||
width: 112rpx;
|
width: 112rpx;
|
||||||
height: 112rpx;
|
height: 112rpx;
|
||||||
border-radius: 38rpx;
|
border-radius: 38rpx;
|
||||||
display: flex;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
background: #ecfeff;
|
||||||
justify-content: center;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 900;
|
|
||||||
background: linear-gradient(180deg, #67e8f9 0%, #0891b2 100%);
|
|
||||||
box-shadow: 0 12rpx 26rpx rgba(8, 145, 178, 0.22);
|
box-shadow: 0 12rpx 26rpx rgba(8, 145, 178, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +171,7 @@ const handleLogout = () => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phone {
|
.nickname {
|
||||||
display: block;
|
display: block;
|
||||||
color: #164e63;
|
color: #164e63;
|
||||||
font-size: 38rpx;
|
font-size: 38rpx;
|
||||||
@ -240,9 +230,15 @@ const handleLogout = () => {
|
|||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bind-btn {
|
.bind-actions {
|
||||||
height: 78rpx;
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
margin-top: 18rpx;
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bind-action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 78rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
border: 2rpx dashed #67e8f9;
|
border: 2rpx dashed #67e8f9;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -251,6 +247,14 @@ const handleLogout = () => {
|
|||||||
color: #0891b2;
|
color: #0891b2;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #0891b2, #06b6d4);
|
||||||
|
box-shadow: 0 12rpx 26rpx rgba(8, 145, 178, 0.22);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-card {
|
.menu-card {
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: activeTab === tab.key }"
|
:class="{ active: activeTab === tab.key }"
|
||||||
@click="activeTab = tab.key"
|
@click="switchTab(tab.key)"
|
||||||
>
|
>
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</view>
|
</view>
|
||||||
@ -77,63 +77,116 @@
|
|||||||
</ParentCard>
|
</ParentCard>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="activeTab === 'answers'">
|
<view v-if="activeTab === 'answers'" class="answers-panel">
|
||||||
<ParentCard class="section-card" title="答题卡详情" subtitle="点击题号查看每道题的答题情况">
|
<ParentCard class="section-card" title="答题卡详情" subtitle="点击题号查看每道题的答题情况">
|
||||||
<view class="answer-legend">
|
<view class="answer-legend">
|
||||||
<view><text class="legend-dot correct"></text>正确</view>
|
<view><text class="legend-dot correct"></text>正确</view>
|
||||||
<view><text class="legend-dot wrong"></text>错误</view>
|
<view><text class="legend-dot wrong"></text>错误</view>
|
||||||
|
<view><text class="legend-dot unanswered"></text>未答</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="question-grid">
|
<view v-if="pageLoading && !questionSummaries.length" class="answers-loading">
|
||||||
|
<xy-Empty type="loading" content="加载答题卡..." />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="questionSummaries.length" class="question-grid">
|
||||||
<view
|
<view
|
||||||
v-for="question in detail.questions"
|
v-for="question in questionSummaries"
|
||||||
:key="question.no"
|
:key="question.no"
|
||||||
class="question-no"
|
class="question-no"
|
||||||
:class="[question.status, { active: selectedNo === question.no }]"
|
:class="[question.status, { active: selectedNo === question.no }]"
|
||||||
@click="selectedNo = question.no"
|
@click="selectQuestion(question.no)"
|
||||||
>
|
>
|
||||||
{{ question.no }}
|
{{ question.no }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<xy-Empty v-else type="task" content="暂无题目数据" />
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
|
|
||||||
<ParentCard class="section-card" title="当前题目详情">
|
<ParentCard v-if="selectedNo && selectedQuestionSummary" class="section-card" title="当前题目详情">
|
||||||
<view class="question-head">
|
<view class="question-head">
|
||||||
<text>第 {{ selectedQuestion.no }} 题</text>
|
<text>第 {{ selectedQuestionSummary.no }} 题</text>
|
||||||
<text>{{ selectedQuestion.type }}</text>
|
<text>{{ selectedQuestionSummary.type }}</text>
|
||||||
<text :class="selectedQuestion.status === 'correct' ? 'status-correct' : 'status-wrong'">
|
<text :class="statusClass(selectedQuestionSummary.status)">
|
||||||
{{ selectedQuestion.status === 'correct' ? '正确' : '错误' }}
|
{{ statusLabel(selectedQuestionSummary.status) }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-fields">
|
<view v-if="detailLoadingNo === selectedNo" class="answers-loading">
|
||||||
|
<xy-Empty type="loading" content="加载题目详情..." />
|
||||||
|
</view>
|
||||||
|
<view v-else-if="selectedQuestionDetail" class="detail-fields">
|
||||||
<view class="field-card">
|
<view class="field-card">
|
||||||
<text class="field-label">题干</text>
|
<text class="field-label">题干</text>
|
||||||
<text class="field-content">{{ selectedQuestion.stem }}</text>
|
<view v-if="selectedQuestionDetail.stemHtml" class="field-content rich-content">
|
||||||
|
<rich-text :nodes="selectedQuestionDetail.stemHtml" />
|
||||||
|
</view>
|
||||||
|
<view v-if="selectedQuestionDetail.stemImages?.length" class="stem-images">
|
||||||
|
<image
|
||||||
|
v-for="(img, imgIdx) in selectedQuestionDetail.stemImages"
|
||||||
|
:key="imgIdx"
|
||||||
|
:src="img"
|
||||||
|
mode="widthFix"
|
||||||
|
class="stem-image"
|
||||||
|
@click="previewStemImage(imgIdx)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text
|
||||||
|
v-if="!selectedQuestionDetail.stemHtml && !selectedQuestionDetail.stemImages?.length"
|
||||||
|
class="field-content"
|
||||||
|
>
|
||||||
|
暂无题干
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="field-card success">
|
<view class="field-card success">
|
||||||
<text class="field-label">正确答案</text>
|
<text class="field-label">正确答案</text>
|
||||||
<text class="field-content">{{ selectedQuestion.correctAnswer }}</text>
|
<view v-if="selectedQuestionDetail.correctAnswerHtml" class="field-content rich-content">
|
||||||
|
<rich-text :nodes="selectedQuestionDetail.correctAnswerHtml" />
|
||||||
|
</view>
|
||||||
|
<text v-else class="field-content">--</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="field-card student">
|
<view class="field-card student">
|
||||||
<text class="field-label">学员答案</text>
|
<text class="field-label">学员答案</text>
|
||||||
<text class="field-content">{{ selectedQuestion.studentAnswer }}</text>
|
<view v-if="selectedQuestionDetail.studentAnswerImages?.length" class="answer-images">
|
||||||
|
<image
|
||||||
|
v-for="(img, imgIdx) in selectedQuestionDetail.studentAnswerImages"
|
||||||
|
:key="imgIdx"
|
||||||
|
:src="img"
|
||||||
|
mode="widthFix"
|
||||||
|
class="answer-image"
|
||||||
|
@click="previewStudentImage(imgIdx)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view v-if="selectedQuestionDetail.studentAnswerHtml" class="field-content rich-content">
|
||||||
|
<rich-text :nodes="selectedQuestionDetail.studentAnswerHtml" />
|
||||||
|
</view>
|
||||||
|
<text
|
||||||
|
v-else-if="selectedQuestionDetail.studentAnswer"
|
||||||
|
class="field-content"
|
||||||
|
>{{ selectedQuestionDetail.studentAnswer }}</text>
|
||||||
|
<text
|
||||||
|
v-else-if="!selectedQuestionDetail.studentAnswerImages?.length"
|
||||||
|
class="field-content"
|
||||||
|
>未作答</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="field-card">
|
<view class="field-card">
|
||||||
<text class="field-label">得分情况</text>
|
<text class="field-label">得分情况</text>
|
||||||
<text class="field-content">{{ selectedQuestion.score }}</text>
|
<text class="field-content">{{ selectedQuestionSummary.score }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="field-card">
|
<view class="field-card">
|
||||||
<text class="field-label">班级得分率</text>
|
<text class="field-label">班级得分率</text>
|
||||||
<text class="field-content">{{ selectedQuestion.classRate }}</text>
|
<text class="field-content">{{ selectedQuestionSummary.classRate }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="field-card">
|
<view class="field-card">
|
||||||
<text class="field-label">学校得分率</text>
|
<text class="field-label">学校得分率</text>
|
||||||
<text class="field-content">{{ selectedQuestion.schoolRate }}</text>
|
<text class="field-content">{{ selectedQuestionSummary.schoolRate }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
|
|
||||||
<ParentCard class="section-card" title="每道题具体情况">
|
<ParentCard class="section-card" title="每道题具体情况">
|
||||||
<scroll-view scroll-x class="question-table-scroll">
|
<view class="table-toggle" @click="showQuestionTable = !showQuestionTable">
|
||||||
|
<text>{{ showQuestionTable ? '收起列表' : '展开查看全部题目' }}</text>
|
||||||
|
<text class="table-toggle-icon">{{ showQuestionTable ? '▲' : '▼' }}</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view v-if="showQuestionTable" scroll-x class="question-table-scroll">
|
||||||
<view class="question-table">
|
<view class="question-table">
|
||||||
<view class="question-row table-head">
|
<view class="question-row table-head">
|
||||||
<text>题号</text>
|
<text>题号</text>
|
||||||
@ -144,12 +197,17 @@
|
|||||||
<text>班级得分率</text>
|
<text>班级得分率</text>
|
||||||
<text>学校得分率</text>
|
<text>学校得分率</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-for="question in detail.questions" :key="`row-${question.no}`" class="question-row">
|
<view
|
||||||
|
v-for="question in questionSummaries"
|
||||||
|
:key="`row-${question.no}`"
|
||||||
|
class="question-row"
|
||||||
|
@click="selectQuestion(question.no)"
|
||||||
|
>
|
||||||
<text>{{ question.no }}</text>
|
<text>{{ question.no }}</text>
|
||||||
<text>{{ question.type }}</text>
|
<text>{{ question.type }}</text>
|
||||||
<text>{{ question.score }}</text>
|
<text>{{ question.score }}</text>
|
||||||
<text :class="question.status === 'correct' ? 'status-correct' : 'status-wrong'">
|
<text :class="statusClass(question.status)">
|
||||||
{{ question.status === 'correct' ? '正确' : '错误' }}
|
{{ statusLabel(question.status) }}
|
||||||
</text>
|
</text>
|
||||||
<text>{{ question.knowledge }}</text>
|
<text>{{ question.knowledge }}</text>
|
||||||
<text>{{ question.classRate }}</text>
|
<text>{{ question.classRate }}</text>
|
||||||
@ -158,9 +216,9 @@
|
|||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
</template>
|
</view>
|
||||||
|
|
||||||
<template v-else>
|
<template v-if="activeTab === 'knowledge'">
|
||||||
<ParentCard class="section-card" title="错题知识点占比" subtitle="用于判断最近应优先补强的知识点">
|
<ParentCard class="section-card" title="错题知识点占比" subtitle="用于判断最近应优先补强的知识点">
|
||||||
<view class="knowledge-summary">
|
<view class="knowledge-summary">
|
||||||
<view class="wrong-total">
|
<view class="wrong-total">
|
||||||
@ -223,9 +281,21 @@
|
|||||||
<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 ParentCard from '../components/ParentCard.vue';
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
import ParentPage from '../components/ParentPage.vue';
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
import { reportDetails, reportList, type ReportKnowledgeScore } from '../utils/data';
|
import {
|
||||||
|
reportDetails,
|
||||||
|
type ReportKnowledgeScore,
|
||||||
|
type ReportQuestion,
|
||||||
|
type ReportQuestionSummary,
|
||||||
|
type ReportScoreBlock,
|
||||||
|
} from '../utils/data';
|
||||||
|
import {
|
||||||
|
mapPaperQuestion,
|
||||||
|
mapPaperQuestionSummaries,
|
||||||
|
normalizeLegacyQuestion,
|
||||||
|
} from '../utils/paperDetail';
|
||||||
|
|
||||||
type DetailTab = 'analysis' | 'answers' | 'knowledge';
|
type DetailTab = 'analysis' | 'answers' | 'knowledge';
|
||||||
type KnowledgeLevel = 'all' | 'weak' | 'pass' | 'good';
|
type KnowledgeLevel = 'all' | 'weak' | 'pass' | 'good';
|
||||||
@ -243,19 +313,210 @@ const knowledgeTabs: { key: KnowledgeLevel; label: string }[] = [
|
|||||||
{ key: 'good', label: '良好' },
|
{ key: 'good', label: '良好' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const reportId = ref('r1');
|
const REPORT_FLAG = 1;
|
||||||
|
|
||||||
|
const recordId = ref('');
|
||||||
|
const reportName = ref('');
|
||||||
|
const reportSubject = ref('');
|
||||||
|
const reportCategory = ref('');
|
||||||
|
const reportDate = ref('');
|
||||||
|
const reportScoreRate = ref('');
|
||||||
|
|
||||||
const activeTab = ref<DetailTab>('analysis');
|
const activeTab = ref<DetailTab>('analysis');
|
||||||
const selectedNo = ref(1);
|
const selectedNo = ref(0);
|
||||||
const activeKnowledgeLevel = ref<KnowledgeLevel>('all');
|
const activeKnowledgeLevel = ref<KnowledgeLevel>('all');
|
||||||
|
const pageLoading = ref(false);
|
||||||
|
const showQuestionTable = ref(false);
|
||||||
|
const detailLoadingNo = ref(0);
|
||||||
|
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>>({});
|
||||||
|
|
||||||
onLoad((query) => {
|
onLoad((query) => {
|
||||||
reportId.value = String(query?.id || 'r1');
|
recordId.value = String(query?.id || '');
|
||||||
|
reportName.value = query?.name ? decodeURIComponent(String(query.name)) : '';
|
||||||
|
reportSubject.value = query?.subject ? decodeURIComponent(String(query.subject)) : '';
|
||||||
|
reportCategory.value = query?.category ? decodeURIComponent(String(query.category)) : '';
|
||||||
|
reportDate.value = String(query?.date || '');
|
||||||
|
reportScoreRate.value = query?.score != null ? String(query.score) : '';
|
||||||
|
loadReportData();
|
||||||
});
|
});
|
||||||
|
|
||||||
const report = computed(() => reportList.find((item) => item.id === reportId.value) || reportList[0]);
|
const mockDetail = computed(() => reportDetails[recordId.value] || reportDetails.r1);
|
||||||
const detail = computed(() => reportDetails[report.value.id] || reportDetails.r1);
|
|
||||||
const selectedQuestion = computed(() => detail.value.questions.find((item) => item.no === selectedNo.value) || detail.value.questions[0]);
|
const formatRank = (rank?: unknown, total?: unknown) => {
|
||||||
const wrongCount = computed(() => detail.value.questions.filter((item) => item.status === 'wrong').length);
|
if (rank == null || rank === '' || total == null || total === '') return '';
|
||||||
|
return `${rank}/${total}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scoreBlocks = computed<ReportScoreBlock[]>(() => {
|
||||||
|
const vo = (jobAnalyse.value?.jobReportVo || {}) as Record<string, unknown>;
|
||||||
|
const mark = Number(vo.markScore ?? 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}%` : '--';
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ label: '我的得分', value: String(mark), desc: `总分 ${total || '--'}` },
|
||||||
|
{ label: '满分', value: String(total || '--'), desc: '本次卷面' },
|
||||||
|
{ label: '得分率', value: rateFromScore, desc: total > 0 && mark / total >= 0.8 ? '超过多数同学' : '' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ label: '班级排名', value: classRank || mockDetail.value.ranks[0]?.value || '--' },
|
||||||
|
{ label: '年级排名', value: gradeRank || mockDetail.value.ranks[1]?.value || '--' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const report = computed(() => ({
|
||||||
|
category: reportCategory.value || getPaperTestTypeLabelFromPaper(),
|
||||||
|
title: reportName.value || String(paperDetail.value.paperName || '作业详情'),
|
||||||
|
date: reportDate.value,
|
||||||
|
subject: reportSubject.value || String(paperDetail.value.subjectName || '多科'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getPaperTestTypeLabelFromPaper = () => {
|
||||||
|
const type = Number(paperDetail.value.type);
|
||||||
|
if (!type) return '';
|
||||||
|
const map: Record<number, string> = {
|
||||||
|
1: '单元测试',
|
||||||
|
2: '周测',
|
||||||
|
3: '月测',
|
||||||
|
4: '随堂练习',
|
||||||
|
5: '课前预习',
|
||||||
|
};
|
||||||
|
return map[type] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const detail = computed(() => ({
|
||||||
|
...mockDetail.value,
|
||||||
|
scoreBlocks: scoreBlocks.value,
|
||||||
|
ranks: ranks.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectedQuestionSummary = computed(
|
||||||
|
() => questionSummaries.value.find((item) => item.no === selectedNo.value) || null,
|
||||||
|
);
|
||||||
|
const selectedQuestionDetail = computed(() =>
|
||||||
|
selectedNo.value ? questionDetailCache.value[selectedNo.value] : undefined,
|
||||||
|
);
|
||||||
|
const wrongCount = computed(
|
||||||
|
() =>
|
||||||
|
questionSummaries.value.filter((item) => item.status === 'wrong' || item.status === 'unanswered').length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusLabel = (status?: ReportQuestion['status']) => {
|
||||||
|
if (status === 'correct') return '正确';
|
||||||
|
if (status === 'unanswered') return '未答';
|
||||||
|
return '错误';
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusClass = (status?: ReportQuestion['status']) => {
|
||||||
|
if (status === 'correct') return 'status-correct';
|
||||||
|
if (status === 'unanswered') return 'status-muted';
|
||||||
|
return 'status-wrong';
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchTab = (key: DetailTab) => {
|
||||||
|
activeTab.value = key;
|
||||||
|
if (key === 'answers') {
|
||||||
|
ensureDefaultQuestion();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 进入答题卡时默认选中并加载第一题 */
|
||||||
|
const ensureDefaultQuestion = () => {
|
||||||
|
if (!questionSummaries.value.length) return;
|
||||||
|
if (!selectedNo.value) {
|
||||||
|
selectedNo.value = questionSummaries.value[0].no;
|
||||||
|
}
|
||||||
|
loadQuestionDetail(selectedNo.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadQuestionDetail = (no: number) => {
|
||||||
|
if (questionDetailCache.value[no]) {
|
||||||
|
if (detailLoadingNo.value === no) detailLoadingNo.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (detailLoadingNo.value === no) return;
|
||||||
|
|
||||||
|
detailLoadingNo.value = no;
|
||||||
|
try {
|
||||||
|
const index = questionSummaries.value.findIndex((item) => item.no === no);
|
||||||
|
if (index >= 0 && rawTitleItems.value[index]) {
|
||||||
|
questionDetailCache.value = {
|
||||||
|
...questionDetailCache.value,
|
||||||
|
[no]: mapPaperQuestion(rawTitleItems.value[index], index),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const mockQuestion = mockDetail.value.questions.find((item) => Number(item.no) === no);
|
||||||
|
if (mockQuestion) {
|
||||||
|
questionDetailCache.value = {
|
||||||
|
...questionDetailCache.value,
|
||||||
|
[no]: normalizeLegacyQuestion(mockQuestion as unknown as Record<string, unknown>),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (detailLoadingNo.value === no) {
|
||||||
|
detailLoadingNo.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectQuestion = (no: number) => {
|
||||||
|
selectedNo.value = no;
|
||||||
|
loadQuestionDetail(no);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadReportData = async () => {
|
||||||
|
if (!recordId.value) return;
|
||||||
|
|
||||||
|
pageLoading.value = true;
|
||||||
|
try {
|
||||||
|
const [detailRes, analyseRes] = await Promise.all([
|
||||||
|
getPaperRecordDetail({ recordId: recordId.value, reportFlag: REPORT_FLAG }),
|
||||||
|
getJobAnalyse({ recordId: recordId.value }).catch(() => ({ data: {} })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
paperDetail.value = (detailRes.data || {}) as Record<string, unknown>;
|
||||||
|
jobAnalyse.value = (analyseRes.data || {}) as Record<string, unknown>;
|
||||||
|
|
||||||
|
const titleList = paperDetail.value.titleVoList || paperDetail.value.subjectTitleVoList;
|
||||||
|
rawTitleItems.value = Array.isArray(titleList) ? (titleList as Record<string, unknown>[]) : [];
|
||||||
|
questionSummaries.value = mapPaperQuestionSummaries(rawTitleItems.value);
|
||||||
|
|
||||||
|
if (!reportName.value) {
|
||||||
|
reportName.value = String(paperDetail.value.paperName || '');
|
||||||
|
}
|
||||||
|
if (!reportSubject.value) {
|
||||||
|
reportSubject.value = String(paperDetail.value.subjectName || '');
|
||||||
|
}
|
||||||
|
if (questionSummaries.value.length && !selectedNo.value) {
|
||||||
|
selectedNo.value = questionSummaries.value[0].no;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[parent-report-detail] load err', err);
|
||||||
|
uni.showToast({ title: '报告加载失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const knowledgeRate = (item: ReportKnowledgeScore) => Math.round((item.score / item.fullScore) * 100);
|
const knowledgeRate = (item: ReportKnowledgeScore) => Math.round((item.score / item.fullScore) * 100);
|
||||||
|
|
||||||
@ -271,6 +532,18 @@ const filteredKnowledgeScores = computed(() => {
|
|||||||
return detail.value.knowledgeScores.filter((item) => knowledgeLevelOf(item) === activeKnowledgeLevel.value);
|
return detail.value.knowledgeScores.filter((item) => knowledgeLevelOf(item) === activeKnowledgeLevel.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const previewStemImage = (index: number) => {
|
||||||
|
const urls = selectedQuestionDetail.value?.stemImages || [];
|
||||||
|
if (!urls.length) return;
|
||||||
|
uni.previewImage({ urls, current: urls[index] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewStudentImage = (index: number) => {
|
||||||
|
const urls = selectedQuestionDetail.value?.studentAnswerImages || [];
|
||||||
|
if (!urls.length) return;
|
||||||
|
uni.previewImage({ urls, current: urls[index] });
|
||||||
|
};
|
||||||
|
|
||||||
const downloadReport = () => {
|
const downloadReport = () => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '报告生成中',
|
title: '报告生成中',
|
||||||
@ -289,6 +562,9 @@ const goHome = () => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 36rpx;
|
gap: 36rpx;
|
||||||
padding-bottom: calc(144rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(144rpx + env(safe-area-inset-bottom));
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-card {
|
.hero-card {
|
||||||
@ -464,6 +740,11 @@ const goHome = () => {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-muted {
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
.answer-legend {
|
.answer-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 28rpx;
|
gap: 28rpx;
|
||||||
@ -491,6 +772,37 @@ const goHome = () => {
|
|||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.legend-dot.unanswered {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answers-loading {
|
||||||
|
min-height: 220rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answers-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-toggle-icon {
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.question-grid {
|
.question-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(6, 1fr);
|
||||||
@ -517,6 +829,12 @@ const goHome = () => {
|
|||||||
background: #fef2f2;
|
background: #fef2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.question-no.unanswered {
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
color: #64748b;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
.question-no.active {
|
.question-no.active {
|
||||||
border-color: #2563eb;
|
border-color: #2563eb;
|
||||||
box-shadow: 0 0 0 4rpx rgba(37, 99, 235, 0.1);
|
box-shadow: 0 0 0 4rpx rgba(37, 99, 235, 0.1);
|
||||||
@ -535,6 +853,7 @@ const goHome = () => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 18rpx;
|
gap: 18rpx;
|
||||||
margin-top: 22rpx;
|
margin-top: 22rpx;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-card {
|
.field-card {
|
||||||
@ -542,6 +861,9 @@ const goHome = () => {
|
|||||||
border: 2rpx solid #dbeafe;
|
border: 2rpx solid #dbeafe;
|
||||||
border-radius: 18rpx;
|
border-radius: 18rpx;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-card.success {
|
.field-card.success {
|
||||||
@ -576,6 +898,64 @@ const goHome = () => {
|
|||||||
color: #334155;
|
color: #334155;
|
||||||
font-size: 25rpx;
|
font-size: 25rpx;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-all;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(.formula-text) {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-family: 'Times New Roman', 'STIXGeneral', Georgia, serif;
|
||||||
|
font-style: italic;
|
||||||
|
color: #1e1b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(.formula-img) {
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(.fill-blank) {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 60rpx;
|
||||||
|
border-bottom: 3rpx solid #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stem-images {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stem-image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-images {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-table-scroll {
|
.question-table-scroll {
|
||||||
|
|||||||
@ -1,38 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<ParentPage active="reports">
|
<ParentPage active="reports" @scrolltolower="loadMore">
|
||||||
<view class="filter-row">
|
<ParentFilterRow
|
||||||
<picker class="filter-picker" :range="subjectOptions" :value="subjectIndex" @change="onSubjectChange">
|
:items="filterItems"
|
||||||
<view class="filter-pill">
|
@change="onFilterChange"
|
||||||
<text>{{ subjectOptions[subjectIndex] }}</text>
|
/>
|
||||||
<view class="filter-arrow"></view>
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
<picker class="filter-picker" :range="gradeOptions" :value="gradeIndex" @change="onGradeChange">
|
|
||||||
<view class="filter-pill">
|
|
||||||
<text>{{ gradeOptions[gradeIndex] }}</text>
|
|
||||||
<view class="filter-arrow"></view>
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
<picker class="filter-picker" :range="categoryOptions" :value="categoryIndex" @change="onCategoryChange">
|
|
||||||
<view class="filter-pill">
|
|
||||||
<text>{{ categoryOptions[categoryIndex] }}</text>
|
|
||||||
<view class="filter-arrow"></view>
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view v-if="filteredReports.length" class="report-list">
|
<view v-if="reportList.length" class="report-list">
|
||||||
<ParentCard
|
<ParentCard
|
||||||
v-for="item in filteredReports"
|
v-for="item in reportList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="report-card"
|
class="report-card"
|
||||||
clickable
|
clickable
|
||||||
@click="openReport(item.id)"
|
@click="openReport(item)"
|
||||||
>
|
>
|
||||||
<view class="report-body">
|
<view class="report-body">
|
||||||
<view class="subject-badge" :class="subjectClass(item.subject)">{{ item.subject }}</view>
|
<view class="subject-badge" :class="subjectClass(item.subject)">{{ subjectShort(item.subject) }}</view>
|
||||||
<view class="report-main">
|
<view class="report-main">
|
||||||
<view class="category">[{{ item.category }}]</view>
|
<view v-if="item.category" class="category">[{{ item.category }}]</view>
|
||||||
<text class="report-title ellipsis-2">{{ item.title }}</text>
|
<text class="report-title ellipsis-2">{{ item.title }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="score-wrap">
|
<view class="score-wrap">
|
||||||
@ -41,91 +25,209 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
|
<view v-if="loadingMore" class="list-tip">加载中...</view>
|
||||||
|
<view v-else-if="!hasMore" class="list-tip">没有更多了</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-else class="empty-card">
|
<view v-else-if="!listLoading" class="empty-card">
|
||||||
<xy-Empty type="task" content="暂无符合条件的考试报告" />
|
<xy-Empty type="task" content="暂无符合条件的考试报告" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="listLoading" class="loading-wrap">
|
||||||
|
<xy-Empty type="loading" content="加载中..." />
|
||||||
|
</view>
|
||||||
</ParentPage>
|
</ParentPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import {
|
||||||
|
getListCompleteRecord,
|
||||||
|
getPaperTestTypeLabel,
|
||||||
|
normalizeListCompleteRecord,
|
||||||
|
PAPER_TEST_TYPE_OPTIONS,
|
||||||
|
type ListCompleteRecordParams,
|
||||||
|
type PaperRecord,
|
||||||
|
} from '@/api/homework';
|
||||||
|
import { useParentStore } from '@/state/modules/parent';
|
||||||
|
import { formatTime } from '@/utils/format';
|
||||||
|
import { SUBJECT_FILTER_OPTIONS, type SubjectFilterOption } from '@/utils/subject';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import ParentCard from '../components/ParentCard.vue';
|
import ParentCard from '../components/ParentCard.vue';
|
||||||
|
import ParentFilterRow from '../components/ParentFilterRow.vue';
|
||||||
import ParentPage from '../components/ParentPage.vue';
|
import ParentPage from '../components/ParentPage.vue';
|
||||||
import { categoryOptions, gradeOptions, reportList, subjectOptions, type ReportItem } from '../utils/data';
|
|
||||||
|
|
||||||
|
interface ReportCard {
|
||||||
|
id: string;
|
||||||
|
subject: string;
|
||||||
|
category: string;
|
||||||
|
title: string;
|
||||||
|
score: number;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentStore = useParentStore();
|
||||||
|
const { activeChild } = storeToRefs(parentStore);
|
||||||
|
|
||||||
|
const typeOptionLabels = PAPER_TEST_TYPE_OPTIONS.map((item) => item.label);
|
||||||
|
|
||||||
|
const subjectOptions = ref<SubjectFilterOption[]>([...SUBJECT_FILTER_OPTIONS]);
|
||||||
const subjectIndex = ref(0);
|
const subjectIndex = ref(0);
|
||||||
const gradeIndex = ref(0);
|
const typeIndex = ref(0);
|
||||||
const categoryIndex = ref(0);
|
|
||||||
|
|
||||||
const filteredReports = computed(() => {
|
const reportList = ref<ReportCard[]>([]);
|
||||||
const subject = subjectOptions[subjectIndex.value];
|
const listLoading = ref(false);
|
||||||
const grade = gradeOptions[gradeIndex.value];
|
const loadingMore = ref(false);
|
||||||
const category = categoryOptions[categoryIndex.value];
|
const hasMore = ref(true);
|
||||||
return reportList.filter((item) => {
|
const currentPage = ref(1);
|
||||||
const subjectMatched = subject === '全部学科' || item.subject === subject;
|
|
||||||
const gradeMatched = grade === '全部年级' || item.grade === grade;
|
const subjectOptionLabels = computed(() => subjectOptions.value.map((item) => item.label));
|
||||||
const categoryMatched = category === '全部类型' || item.category === category;
|
|
||||||
return subjectMatched && gradeMatched && categoryMatched;
|
const filterItems = computed(() => [
|
||||||
});
|
{ key: 'subject', range: subjectOptionLabels.value, value: subjectIndex.value },
|
||||||
|
{ key: 'type', range: typeOptionLabels, value: typeIndex.value },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getScoreRate = (item: PaperRecord) => {
|
||||||
|
const total = Number(item.totalScore);
|
||||||
|
const score = Number(item.score);
|
||||||
|
if (total > 0) {
|
||||||
|
return Math.round((score / total) * 1000) / 10;
|
||||||
|
}
|
||||||
|
return score || 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapRecord = (item: PaperRecord): ReportCard => ({
|
||||||
|
id: String(item.id),
|
||||||
|
subject: item.subjectName || '多科',
|
||||||
|
category: getPaperTestTypeLabel(item.type),
|
||||||
|
title: item.paperName || '',
|
||||||
|
score: getScoreRate(item),
|
||||||
|
date: formatTime(item.markTime || item.submitTime || item.endTime, 'YYYY-MM-DD'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubjectChange = (e: any) => { subjectIndex.value = Number(e.detail.value); };
|
const buildRequestParams = (): ListCompleteRecordParams => {
|
||||||
const onGradeChange = (e: any) => { gradeIndex.value = Number(e.detail.value); };
|
const params: ListCompleteRecordParams = {
|
||||||
const onCategoryChange = (e: any) => { categoryIndex.value = Number(e.detail.value); };
|
current: currentPage.value,
|
||||||
|
resourceType: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (activeChild.value.id) {
|
||||||
|
params.userId = activeChild.value.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subject = subjectOptions.value[subjectIndex.value];
|
||||||
|
if (subject?.subjectId) {
|
||||||
|
params.subjectId = subject.subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeOption = PAPER_TEST_TYPE_OPTIONS[typeIndex.value];
|
||||||
|
if (typeOption?.value) {
|
||||||
|
params.type = typeOption.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchReports = async (append = false) => {
|
||||||
|
if (!activeChild.value.id) {
|
||||||
|
reportList.value = [];
|
||||||
|
hasMore.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getListCompleteRecord(buildRequestParams());
|
||||||
|
const { rows, totalRows } = normalizeListCompleteRecord(res.data);
|
||||||
|
const mapped = rows.map(mapRecord);
|
||||||
|
|
||||||
|
if (append) {
|
||||||
|
const exists = new Set(reportList.value.map((item) => item.id));
|
||||||
|
reportList.value = reportList.value.concat(mapped.filter((item) => !exists.has(item.id)));
|
||||||
|
} else {
|
||||||
|
reportList.value = mapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMore.value = reportList.value.length < totalRows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshReports = async () => {
|
||||||
|
listLoading.value = true;
|
||||||
|
currentPage.value = 1;
|
||||||
|
hasMore.value = true;
|
||||||
|
try {
|
||||||
|
await fetchReports(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[parent-reports] fetch err', err);
|
||||||
|
reportList.value = [];
|
||||||
|
hasMore.value = false;
|
||||||
|
} finally {
|
||||||
|
listLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
if (listLoading.value || loadingMore.value || !hasMore.value) return;
|
||||||
|
loadingMore.value = true;
|
||||||
|
currentPage.value += 1;
|
||||||
|
try {
|
||||||
|
await fetchReports(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[parent-reports] load more err', err);
|
||||||
|
currentPage.value -= 1;
|
||||||
|
} finally {
|
||||||
|
loadingMore.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubjectChange = (e: { detail: { value: string } }) => {
|
||||||
|
subjectIndex.value = Number(e.detail.value);
|
||||||
|
refreshReports();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTypeChange = (e: { detail: { value: string } }) => {
|
||||||
|
typeIndex.value = Number(e.detail.value);
|
||||||
|
refreshReports();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilterChange = (payload: { key: string; value: number }) => {
|
||||||
|
if (payload.key === 'subject') {
|
||||||
|
subjectIndex.value = payload.value;
|
||||||
|
refreshReports();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (payload.key === 'type') {
|
||||||
|
typeIndex.value = payload.value;
|
||||||
|
refreshReports();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const formatScore = (score: number) => `${score.toFixed(1).replace(/\.0$/, '')}%`;
|
const formatScore = (score: number) => `${score.toFixed(1).replace(/\.0$/, '')}%`;
|
||||||
|
|
||||||
const subjectClass = (subject: ReportItem['subject']) => {
|
const subjectShort = (subject: string) => {
|
||||||
if (subject === '数学') return 'math';
|
if (!subject) return '多科';
|
||||||
if (subject === '语文') return 'chinese';
|
return subject.length > 2 ? subject.slice(0, 2) : subject;
|
||||||
if (subject === '英语') return 'english';
|
};
|
||||||
|
|
||||||
|
const subjectClass = (subject: string) => {
|
||||||
|
if (subject.includes('数学')) return 'math';
|
||||||
|
if (subject.includes('语文')) return 'chinese';
|
||||||
|
if (subject.includes('英语')) return 'english';
|
||||||
return 'multi';
|
return 'multi';
|
||||||
};
|
};
|
||||||
|
|
||||||
const openReport = (id: string) => {
|
const openReport = (item: ReportCard) => {
|
||||||
uni.navigateTo({ url: `/pages/parent/reports/detail?id=${id}` });
|
uni.navigateTo({
|
||||||
|
url: `/pages/parent/reports/detail?id=${item.id}&name=${encodeURIComponent(item.title)}&subject=${encodeURIComponent(item.subject)}&category=${encodeURIComponent(item.category)}&date=${item.date}&score=${item.score}`,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
refreshReports();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.filter-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 16rpx;
|
|
||||||
padding-top: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-picker {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-pill {
|
|
||||||
height: 72rpx;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10rpx;
|
|
||||||
color: #164e63;
|
|
||||||
font-size: 24rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
background: rgba(255, 255, 255, 0.94);
|
|
||||||
box-shadow:
|
|
||||||
0 8rpx 22rpx rgba(8, 145, 178, 0.08),
|
|
||||||
inset 0 -2rpx 0 rgba(207, 250, 254, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-arrow {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 8rpx solid transparent;
|
|
||||||
border-right: 8rpx solid transparent;
|
|
||||||
border-top: 10rpx solid #0891b2;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-list {
|
.report-list {
|
||||||
margin-top: 22rpx;
|
margin-top: 22rpx;
|
||||||
}
|
}
|
||||||
@ -203,4 +305,16 @@ const openReport = (id: string) => {
|
|||||||
height: 520rpx;
|
height: 520rpx;
|
||||||
margin-top: 24rpx;
|
margin-top: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-wrap {
|
||||||
|
height: 520rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-tip {
|
||||||
|
text-align: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 24rpx;
|
||||||
|
padding: 12rpx 0 24rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { SUBJECT_FILTER_OPTIONS } from '@/utils/subject';
|
||||||
|
|
||||||
export interface ParentChild {
|
export interface ParentChild {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -53,14 +55,29 @@ export interface ReportDifficultyRow {
|
|||||||
rate: string;
|
rate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 答题卡列表用的轻量字段(不含 HTML) */
|
||||||
|
export type ReportQuestionSummary = Pick<
|
||||||
|
ReportQuestion,
|
||||||
|
'no' | 'type' | 'score' | 'status' | 'classRate' | 'schoolRate' | 'knowledge'
|
||||||
|
>;
|
||||||
|
|
||||||
export interface ReportQuestion {
|
export interface ReportQuestion {
|
||||||
no: number;
|
no: number;
|
||||||
type: string;
|
type: string;
|
||||||
score: string;
|
score: string;
|
||||||
status: 'correct' | 'wrong';
|
status: 'correct' | 'wrong' | 'unanswered';
|
||||||
stem: string;
|
/** 供 rich-text 渲染的题干 HTML */
|
||||||
correctAnswer: string;
|
stemHtml: string;
|
||||||
|
/** 题干中的图片(与学生端一致,从 HTML 中拆出单独展示) */
|
||||||
|
stemImages: string[];
|
||||||
|
/** 供 rich-text 渲染的正确答案 HTML */
|
||||||
|
correctAnswerHtml: string;
|
||||||
|
/** 纯文本学员答案(选择题等) */
|
||||||
studentAnswer: string;
|
studentAnswer: string;
|
||||||
|
/** 供 rich-text 渲染的学员答案 HTML(富文本作答) */
|
||||||
|
studentAnswerHtml: string;
|
||||||
|
/** 学员上传的图片答案 */
|
||||||
|
studentAnswerImages: string[];
|
||||||
classRate: string;
|
classRate: string;
|
||||||
schoolRate: string;
|
schoolRate: string;
|
||||||
knowledge: string;
|
knowledge: string;
|
||||||
@ -271,7 +288,7 @@ export const reportDetails: Record<string, ReportDetail> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const subjectOptions = ['全部学科', '数学', '语文', '英语', '多科'];
|
export const subjectOptions = SUBJECT_FILTER_OPTIONS.map((item) => item.label);
|
||||||
export const gradeOptions = ['全部年级', '初一', '初二', '初三'];
|
export const gradeOptions = ['全部年级', '初一', '初二', '初三'];
|
||||||
export const categoryOptions = ['全部类型', '周作业', '月考'];
|
export const categoryOptions = ['全部类型', '周作业', '月考'];
|
||||||
|
|
||||||
|
|||||||
169
src/pages/parent/utils/paperDetail.ts
Normal file
169
src/pages/parent/utils/paperDetail.ts
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import {
|
||||||
|
extractAnswerImages,
|
||||||
|
extractTitleImages,
|
||||||
|
formatQuestionHtml,
|
||||||
|
getQuestionAnswerSource,
|
||||||
|
getQuestionStemSource,
|
||||||
|
isHtmlContent,
|
||||||
|
} from '@/utils/questionHtml';
|
||||||
|
import type { ReportQuestion, ReportQuestionSummary } from './data';
|
||||||
|
|
||||||
|
export type QuestionStatus = 'correct' | 'wrong' | 'unanswered';
|
||||||
|
|
||||||
|
export const normalizeAnswer = (value: unknown) => {
|
||||||
|
if (Array.isArray(value)) return value.join('、');
|
||||||
|
if (value == null) return '';
|
||||||
|
return String(value)
|
||||||
|
.replace(/^\[|\]$/g, '')
|
||||||
|
.replace(/"/g, '')
|
||||||
|
.replace(/,/g, '、');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatRateText = (value: unknown) => {
|
||||||
|
if (value == null || value === '') return '--';
|
||||||
|
const num = Number(value);
|
||||||
|
if (!Number.isNaN(num)) {
|
||||||
|
const rate = num <= 1 ? num * 100 : num;
|
||||||
|
return `${Math.round(rate * 10) / 10}%`;
|
||||||
|
}
|
||||||
|
const text = String(value);
|
||||||
|
return text.includes('%') ? text : `${text}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuestionStatus = (item: Record<string, unknown>): QuestionStatus => {
|
||||||
|
const correct =
|
||||||
|
item.correctResult === 1 ||
|
||||||
|
item.correctResult === true ||
|
||||||
|
item.markFlag === 1 ||
|
||||||
|
item.markFlag === true;
|
||||||
|
if (correct) return 'correct';
|
||||||
|
if (!item.submitAnswer && !item.submitAnswerPic) return 'unanswered';
|
||||||
|
return 'wrong';
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripImageUrlsFromText = (text: string) => {
|
||||||
|
let result = text;
|
||||||
|
const links = text.match(/https?:\/\/[^\s,]+/gi) || [];
|
||||||
|
links.forEach((link) => {
|
||||||
|
result = result.replace(link, '');
|
||||||
|
});
|
||||||
|
return result.replace(/[,,、\s]+$/g, '').replace(/^[,,、\s]+/g, '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildStudentAnswer = (item: Record<string, unknown>) => {
|
||||||
|
const studentAnswerImages = extractAnswerImages(item);
|
||||||
|
let textAnswer = normalizeAnswer(item.submitAnswer);
|
||||||
|
|
||||||
|
if (textAnswer) {
|
||||||
|
const onlyUrls = stripImageUrlsFromText(textAnswer) === '' && studentAnswerImages.length > 0;
|
||||||
|
if (onlyUrls) {
|
||||||
|
textAnswer = '';
|
||||||
|
} else {
|
||||||
|
textAnswer = stripImageUrlsFromText(textAnswer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!textAnswer && !studentAnswerImages.length) {
|
||||||
|
return { studentAnswer: '未作答', studentAnswerHtml: '', studentAnswerImages: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textAnswer && isHtmlContent(textAnswer)) {
|
||||||
|
return {
|
||||||
|
studentAnswer: '',
|
||||||
|
studentAnswerHtml: formatQuestionHtml(textAnswer),
|
||||||
|
studentAnswerImages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
studentAnswer: textAnswer,
|
||||||
|
studentAnswerHtml: '',
|
||||||
|
studentAnswerImages,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getQuestionFullScore = (item: Record<string, unknown>) => Number(item.score ?? item.titleScore ?? 0);
|
||||||
|
|
||||||
|
const getQuestionActualScore = (item: Record<string, unknown>) =>
|
||||||
|
Number(item.actualScore ?? item.markScore ?? item.answerScore ?? item.gotScore ?? 0);
|
||||||
|
|
||||||
|
const formatQuestionScoreText = (item: Record<string, unknown>) => {
|
||||||
|
const fullScore = getQuestionFullScore(item);
|
||||||
|
const actualScore = getQuestionActualScore(item);
|
||||||
|
return `${actualScore}/${fullScore || actualScore || 0}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapPaperQuestionSummary = (item: Record<string, unknown>, index: number): ReportQuestionSummary => {
|
||||||
|
return {
|
||||||
|
no: Number(item.titleSort ?? item.sort ?? index + 1),
|
||||||
|
type: String(item.titleChannelTypeName || item.chanelTypeName || '综合题'),
|
||||||
|
score: formatQuestionScoreText(item),
|
||||||
|
status: getQuestionStatus(item),
|
||||||
|
classRate: formatRateText(
|
||||||
|
item.classScoreRate ?? item.classRate ?? item.classAvgScoreRate ?? item.classGotScoreRate,
|
||||||
|
),
|
||||||
|
schoolRate: formatRateText(
|
||||||
|
item.schoolScoreRate ?? item.schoolRate ?? item.schoolAvgScoreRate ?? item.schoolGotScoreRate,
|
||||||
|
),
|
||||||
|
knowledge: String(item.knowledgeName || item.knowledge || item.knowledgePointName || '待归类'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapPaperQuestionSummaries = (list: unknown[]) =>
|
||||||
|
(Array.isArray(list) ? list : []).map((item, index) =>
|
||||||
|
mapPaperQuestionSummary(item as Record<string, unknown>, index),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mapPaperQuestion = (item: Record<string, unknown>, index: number): ReportQuestion => {
|
||||||
|
const status = getQuestionStatus(item);
|
||||||
|
const stemSource = getQuestionStemSource(item);
|
||||||
|
const answerSource = getQuestionAnswerSource(item);
|
||||||
|
const { studentAnswer, studentAnswerHtml, studentAnswerImages } = buildStudentAnswer(item);
|
||||||
|
const stemImages = extractTitleImages(stemSource);
|
||||||
|
|
||||||
|
return {
|
||||||
|
no: Number(item.titleSort ?? item.sort ?? index + 1),
|
||||||
|
type: String(item.titleChannelTypeName || item.chanelTypeName || '综合题'),
|
||||||
|
score: formatQuestionScoreText(item),
|
||||||
|
status,
|
||||||
|
stemHtml: formatQuestionHtml(stemSource, { stripImages: true }),
|
||||||
|
stemImages,
|
||||||
|
correctAnswerHtml: formatQuestionHtml(answerSource),
|
||||||
|
studentAnswer,
|
||||||
|
studentAnswerHtml,
|
||||||
|
studentAnswerImages,
|
||||||
|
classRate: formatRateText(
|
||||||
|
item.classScoreRate ?? item.classRate ?? item.classAvgScoreRate ?? item.classGotScoreRate,
|
||||||
|
),
|
||||||
|
schoolRate: formatRateText(
|
||||||
|
item.schoolScoreRate ?? item.schoolRate ?? item.schoolAvgScoreRate ?? item.schoolGotScoreRate,
|
||||||
|
),
|
||||||
|
knowledge: String(item.knowledgeName || item.knowledge || item.knowledgePointName || '待归类'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapPaperQuestions = (list: unknown[]) =>
|
||||||
|
(Array.isArray(list) ? list : []).map((item, index) =>
|
||||||
|
mapPaperQuestion(item as Record<string, unknown>, index),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 兼容旧版 mock 数据结构 */
|
||||||
|
export const normalizeLegacyQuestion = (item: Record<string, unknown>): ReportQuestion => {
|
||||||
|
const legacy = item as ReportQuestion & { stem?: string; correctAnswer?: string };
|
||||||
|
if (legacy.stemHtml) return legacy as ReportQuestion;
|
||||||
|
return {
|
||||||
|
no: Number(legacy.no),
|
||||||
|
type: String(legacy.type),
|
||||||
|
score: String(legacy.score),
|
||||||
|
status: legacy.status,
|
||||||
|
stemHtml: String(legacy.stem || ''),
|
||||||
|
stemImages: [],
|
||||||
|
correctAnswerHtml: String(legacy.correctAnswer || ''),
|
||||||
|
studentAnswer: String(legacy.studentAnswer || ''),
|
||||||
|
studentAnswerHtml: '',
|
||||||
|
studentAnswerImages: [],
|
||||||
|
classRate: String(legacy.classRate),
|
||||||
|
schoolRate: String(legacy.schoolRate),
|
||||||
|
knowledge: String(legacy.knowledge),
|
||||||
|
};
|
||||||
|
};
|
||||||
28
src/pages/parent/utils/parentIdentity.ts
Normal file
28
src/pages/parent/utils/parentIdentity.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/** 家长与孩子身份关系(与 xuexiaole-mobile-v 一致) */
|
||||||
|
export const PARENT_TYPE_FATHER = 1;
|
||||||
|
export const PARENT_TYPE_MOTHER = 2;
|
||||||
|
export const PARENT_TYPE_GRANDFATHER = 3;
|
||||||
|
export const PARENT_TYPE_GRANDMOTHER = 4;
|
||||||
|
export const PARENT_TYPE_GRANDFATHER2 = 5;
|
||||||
|
export const PARENT_TYPE_GRANDMOTHER2 = 6;
|
||||||
|
export const PARENT_TYPE_TEACHER = 7;
|
||||||
|
export const PARENT_TYPE_OTHER = 8;
|
||||||
|
|
||||||
|
const OSS_BASE = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/bindDevice';
|
||||||
|
|
||||||
|
export interface ParentIdentityOption {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PARENT_IDENTITY_LIST: ParentIdentityOption[] = [
|
||||||
|
{ name: '爸爸', icon: `${OSS_BASE}/baba.png`, id: PARENT_TYPE_FATHER },
|
||||||
|
{ name: '妈妈', icon: `${OSS_BASE}/mama.png`, id: PARENT_TYPE_MOTHER },
|
||||||
|
{ name: '爷爷', icon: `${OSS_BASE}/yeye.png`, id: PARENT_TYPE_GRANDFATHER },
|
||||||
|
{ name: '奶奶', icon: `${OSS_BASE}/nainai.png`, id: PARENT_TYPE_GRANDMOTHER },
|
||||||
|
{ name: '外公', icon: `${OSS_BASE}/wg.png`, id: PARENT_TYPE_GRANDFATHER2 },
|
||||||
|
{ name: '外婆', icon: `${OSS_BASE}/waipo.png`, id: PARENT_TYPE_GRANDMOTHER2 },
|
||||||
|
{ name: '导学师', icon: `${OSS_BASE}/qita.png`, id: PARENT_TYPE_TEACHER },
|
||||||
|
{ name: '其他', icon: `${OSS_BASE}/qita.png`, id: PARENT_TYPE_OTHER },
|
||||||
|
];
|
||||||
45
src/pages/parent/utils/scanBind.ts
Normal file
45
src/pages/parent/utils/scanBind.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/** 解析扫码结果(与 xuexiaole-mobile-v useScanBind 一致) */
|
||||||
|
export const parseScanBindResult = (raw: string) => {
|
||||||
|
if (!raw?.trim()) {
|
||||||
|
throw new Error('无法识别扫描结果');
|
||||||
|
}
|
||||||
|
let json: Record<string, any>;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
throw new Error('无法识别扫描结果');
|
||||||
|
}
|
||||||
|
if (!json?.account || !json?.userId || !json?.simSerialNumber || !json?.model) {
|
||||||
|
throw new Error('无法识别扫描结果');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
account: String(json.account),
|
||||||
|
childId: String(json.userId),
|
||||||
|
simSerialNumber: String(json.simSerialNumber),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 小程序扫码并跳转绑定确认页 */
|
||||||
|
export const scanAndGoBind = () => {
|
||||||
|
uni.scanCode({
|
||||||
|
onlyFromCamera: false,
|
||||||
|
success: (res) => {
|
||||||
|
try {
|
||||||
|
const parsed = parseScanBindResult(res.result || '');
|
||||||
|
const query = [
|
||||||
|
`childId=${encodeURIComponent(parsed.childId)}`,
|
||||||
|
`account=${encodeURIComponent(parsed.account)}`,
|
||||||
|
`simSerialNumber=${encodeURIComponent(parsed.simSerialNumber)}`,
|
||||||
|
].join('&');
|
||||||
|
uni.navigateTo({ url: `/pages/parent/bind/index?${query}` });
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.showToast({ title: e?.message || '无法识别扫描结果', icon: 'none' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
const msg = err?.errMsg || '';
|
||||||
|
if (msg.includes('cancel')) return;
|
||||||
|
uni.showToast({ title: '扫码失败', icon: 'none' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@
|
|||||||
import type { Router } from 'uni-mini-router';
|
import type { Router } from 'uni-mini-router';
|
||||||
import { useUserStore } from '@/state/modules/user';
|
import { useUserStore } from '@/state/modules/user';
|
||||||
import { getCache } from '@/utils/cache';
|
import { getCache } from '@/utils/cache';
|
||||||
|
import { getStoredRole } from '@/utils/roleHome';
|
||||||
|
|
||||||
// 免登录白名单(路由 name)
|
// 免登录白名单(路由 name)
|
||||||
const whiteList = ['home', 'login', 'parent-home', 'parent-reports', 'parent-profile'];
|
const whiteList = ['home', 'login', 'parent-home', 'parent-reports', 'parent-profile'];
|
||||||
@ -15,20 +16,23 @@ export function userRouternext(router: Router) {
|
|||||||
if (userStore.isLogin) {
|
if (userStore.isLogin) {
|
||||||
const currentRole = getCache('role');
|
const currentRole = getCache('role');
|
||||||
const targetRole = (to as any).meta?.role;
|
const targetRole = (to as any).meta?.role;
|
||||||
if (currentRole === 'teacher' && to.name === 'home') {
|
if (currentRole && to.name === 'home') {
|
||||||
return next({
|
const homeName = currentRole === 'teacher'
|
||||||
name: 'teacher-home',
|
? 'teacher-home'
|
||||||
});
|
: currentRole === 'parent'
|
||||||
}
|
? 'parent-home'
|
||||||
if (currentRole === 'parent' && to.name === 'home') {
|
: 'home';
|
||||||
return next({
|
if (homeName !== 'home') {
|
||||||
name: 'parent-home',
|
return next({ name: homeName });
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
if (targetRole && currentRole && targetRole !== currentRole) {
|
if (targetRole && currentRole && targetRole !== currentRole) {
|
||||||
return next({
|
const homeName = currentRole === 'teacher'
|
||||||
name: currentRole === 'teacher' ? 'teacher-home' : currentRole === 'parent' ? 'parent-home' : 'home',
|
? 'teacher-home'
|
||||||
});
|
: currentRole === 'parent'
|
||||||
|
? 'parent-home'
|
||||||
|
: 'home';
|
||||||
|
return next({ name: homeName });
|
||||||
}
|
}
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
@ -45,9 +49,10 @@ export function userRouternext(router: Router) {
|
|||||||
router.afterEach((to) => {
|
router.afterEach((to) => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
if (userStore.isLogin && to?.name === 'login') {
|
if (userStore.isLogin && to?.name === 'login') {
|
||||||
const role = getCache('role');
|
const role = getStoredRole();
|
||||||
if (!role) return;
|
if (!role) return;
|
||||||
router.replaceAll({ name: role === 'teacher' ? 'teacher-home' : role === 'parent' ? 'parent-home' : 'home' });
|
const homeName = role === 'teacher' ? 'teacher-home' : role === 'parent' ? 'parent-home' : 'home';
|
||||||
|
router.replaceAll({ name: homeName });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
127
src/state/modules/parent.ts
Normal file
127
src/state/modules/parent.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { getParentBindChildList, type ParentBindChildItem } from '@/api/parent';
|
||||||
|
import { getCache, removeCache, setCache } from '@/utils/cache';
|
||||||
|
|
||||||
|
export interface ParentChildItem {
|
||||||
|
bindId: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
account: string;
|
||||||
|
className: string;
|
||||||
|
school: string;
|
||||||
|
examNo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_ACTIVE_CHILD_ID = 'parentActiveChildId';
|
||||||
|
|
||||||
|
const emptyChild = (): ParentChildItem => ({
|
||||||
|
bindId: '',
|
||||||
|
id: '',
|
||||||
|
name: '--',
|
||||||
|
account: '',
|
||||||
|
className: '--',
|
||||||
|
school: '--',
|
||||||
|
examNo: '--',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapBindRow = (row: ParentBindChildItem): ParentChildItem => {
|
||||||
|
const account = row.child?.account || '';
|
||||||
|
return {
|
||||||
|
bindId: String(row.id ?? row.child?.id ?? ''),
|
||||||
|
id: String(row.child?.id ?? row.id ?? ''),
|
||||||
|
name: row.child?.name || row.child?.nickname || '未命名',
|
||||||
|
account,
|
||||||
|
className: row.child?.gradeName || '',
|
||||||
|
school: '',
|
||||||
|
examNo: account,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 家长端 - 绑定孩子列表与当前选中孩子
|
||||||
|
*/
|
||||||
|
export const useParentStore = defineStore('parent', () => {
|
||||||
|
const childrenList = ref<ParentChildItem[]>([]);
|
||||||
|
const activeChildId = ref<string>(getCache(CACHE_ACTIVE_CHILD_ID) || '');
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const activeChild = computed(() => {
|
||||||
|
if (!childrenList.value.length) {
|
||||||
|
return emptyChild();
|
||||||
|
}
|
||||||
|
const matched = childrenList.value.find((item) => item.id === activeChildId.value);
|
||||||
|
return matched || childrenList.value[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeChildIndex = computed(() => {
|
||||||
|
const index = childrenList.value.findIndex((item) => item.id === activeChildId.value);
|
||||||
|
return index >= 0 ? index : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasActiveChild = computed(() => !!activeChild.value.id);
|
||||||
|
|
||||||
|
const ensureActiveChild = () => {
|
||||||
|
if (!childrenList.value.length) {
|
||||||
|
activeChildId.value = '';
|
||||||
|
removeCache(CACHE_ACTIVE_CHILD_ID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exists = childrenList.value.some((item) => item.id === activeChildId.value);
|
||||||
|
if (!activeChildId.value || !exists) {
|
||||||
|
activeChildId.value = childrenList.value[0].id;
|
||||||
|
setCache(CACHE_ACTIVE_CHILD_ID, activeChildId.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectChildByIndex = (index: number) => {
|
||||||
|
const child = childrenList.value[index];
|
||||||
|
if (!child) return;
|
||||||
|
activeChildId.value = child.id;
|
||||||
|
setCache(CACHE_ACTIVE_CHILD_ID, child.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchBindChildren = async (parentId?: string | number) => {
|
||||||
|
const pid = parentId ?? '';
|
||||||
|
if (pid === '' || pid == null) {
|
||||||
|
childrenList.value = [];
|
||||||
|
ensureActiveChild();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getParentBindChildList({ parentId: pid });
|
||||||
|
const rows = res.data?.rows || [];
|
||||||
|
childrenList.value = rows.map(mapBindRow);
|
||||||
|
ensureActiveChild();
|
||||||
|
return childrenList.value;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[parent] fetchBindChildren error', err);
|
||||||
|
childrenList.value = [];
|
||||||
|
ensureActiveChild();
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
childrenList.value = [];
|
||||||
|
activeChildId.value = '';
|
||||||
|
removeCache(CACHE_ACTIVE_CHILD_ID);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
childrenList,
|
||||||
|
activeChildId,
|
||||||
|
activeChild,
|
||||||
|
activeChildIndex,
|
||||||
|
hasActiveChild,
|
||||||
|
loading,
|
||||||
|
fetchBindChildren,
|
||||||
|
selectChildByIndex,
|
||||||
|
ensureActiveChild,
|
||||||
|
clear,
|
||||||
|
};
|
||||||
|
});
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { setCache, getCache, removeCache } from '@/utils/cache';
|
import { setCache, getCache, removeCache } from '@/utils/cache';
|
||||||
|
import { useParentStore } from '@/state/modules/parent';
|
||||||
import {
|
import {
|
||||||
psdLogin as psdLoginApi,
|
psdLogin as psdLoginApi,
|
||||||
parentSmsLogin as parentSmsLoginApi,
|
parentSmsLogin as parentSmsLoginApi,
|
||||||
getUserInfo as getUserInfoApi,
|
getUserInfo as getUserInfoApi,
|
||||||
|
selectUser as selectUserApi,
|
||||||
logout as logoutApi,
|
logout as logoutApi,
|
||||||
type AccountLoginParams,
|
type AccountLoginParams,
|
||||||
type ParentSmsLoginParams,
|
type ParentSmsLoginParams,
|
||||||
@ -43,6 +45,8 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
removeCache('userInfo');
|
removeCache('userInfo');
|
||||||
removeCache('teacherInfo');
|
removeCache('teacherInfo');
|
||||||
removeCache('role');
|
removeCache('role');
|
||||||
|
removeCache('parentActiveChildId');
|
||||||
|
useParentStore().clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 拉取并解析用户信息 */
|
/** 拉取并解析用户信息 */
|
||||||
@ -90,6 +94,20 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
await fetchUserInfo();
|
await fetchUserInfo();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 家长端 - 拉取并保存当前用户信息 */
|
||||||
|
const fetchParentUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const res = await selectUserApi();
|
||||||
|
const info: UserInfo = res.data || {};
|
||||||
|
userInfo.value = info;
|
||||||
|
setCache('userInfo', info);
|
||||||
|
return info;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[user] fetchParentUserInfo error', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/** 家长端 - 保存登录 token */
|
/** 家长端 - 保存登录 token */
|
||||||
const saveParentSession = (accessToken: string) => {
|
const saveParentSession = (accessToken: string) => {
|
||||||
if (!accessToken?.trim()) {
|
if (!accessToken?.trim()) {
|
||||||
@ -139,6 +157,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
parentMpLoginSuccess,
|
parentMpLoginSuccess,
|
||||||
parentLogin,
|
parentLogin,
|
||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
|
fetchParentUserInfo,
|
||||||
logout,
|
logout,
|
||||||
clear,
|
clear,
|
||||||
updateUserInfo,
|
updateUserInfo,
|
||||||
|
|||||||
227
src/utils/questionHtml.ts
Normal file
227
src/utils/questionHtml.ts
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
/** 题目 HTML 处理(与学生端 doWork/Question 保持一致) */
|
||||||
|
|
||||||
|
const LATEX_RENDER_URL = 'https://latex.codecogs.com/svg.image?';
|
||||||
|
|
||||||
|
export const fixMathSymbolsInHtml = (html: string): string => {
|
||||||
|
if (html == null || typeof html !== 'string') return '';
|
||||||
|
let t = html;
|
||||||
|
t = t.replace(/\uFE63/g, '\u2212');
|
||||||
|
t = t.replace(/<(?![a-zA-Z/!])/g, '<');
|
||||||
|
t = t.replace(/ > /g, ' > ');
|
||||||
|
t = t.replace(/AUB/g, 'A∪B');
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
|
||||||
|
const latexToImageUrl = (latex: string): string => {
|
||||||
|
if (!latex) return '';
|
||||||
|
let formula = latex.trim();
|
||||||
|
|
||||||
|
const punctuationMap: Record<string, string> = {
|
||||||
|
',': ',', '。': '.', ':': ':', ';': ';', '!': '!', '?': '?',
|
||||||
|
'(': '(', ')': ')', '【': '[', '】': ']', '{': '{', '}': '}',
|
||||||
|
'《': '<', '》': '>',
|
||||||
|
"\u201c": '"', "\u201d": '"', "\u2018": "'", "\u2019": "'",
|
||||||
|
'+': '+', '-': '-', '\u2212': '-', '×': '*', '÷': '/',
|
||||||
|
'=': '=', '<': '<', '>': '>', '%': '%', '&': '&',
|
||||||
|
'*': '*', '@': '@', '#': '#', '$': '$', '^': '^',
|
||||||
|
'~': '~', '|': '|', '\': '\\', '/': '/', ' ': ' ',
|
||||||
|
};
|
||||||
|
for (const [cn, en] of Object.entries(punctuationMap)) {
|
||||||
|
formula = formula.split(cn).join(en);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mathSymbolMap: Record<string, string> = {
|
||||||
|
'∈': ' \\in ', '∉': ' \\notin ', '⊂': ' \\subset ', '⊃': ' \\supset ',
|
||||||
|
'⊆': ' \\subseteq ', '⊇': ' \\supseteq ', '∪': ' \\cup ', '∩': ' \\cap ',
|
||||||
|
'∅': ' \\emptyset ',
|
||||||
|
'≤': ' \\leq ', '≥': ' \\geq ', '≠': ' \\neq ', '≈': ' \\approx ',
|
||||||
|
'≡': ' \\equiv ', '∝': ' \\propto ',
|
||||||
|
'±': ' \\pm ', '∓': ' \\mp ', '×': ' \\times ', '÷': ' \\div ',
|
||||||
|
'·': ' \\cdot ', '∘': ' \\circ ',
|
||||||
|
'α': '\\alpha ', 'β': '\\beta ', 'γ': '\\gamma ', 'δ': '\\delta ',
|
||||||
|
'ε': '\\varepsilon ', 'θ': '\\theta ', 'λ': '\\lambda ', 'μ': '\\mu ',
|
||||||
|
'σ': '\\sigma ', 'φ': '\\varphi ', 'ω': '\\omega ',
|
||||||
|
'Δ': '\\Delta ', 'Σ': '\\Sigma ', 'Ω': '\\Omega ',
|
||||||
|
'∞': '\\infty ', '∂': '\\partial ', '∇': '\\nabla ', '∫': '\\int ',
|
||||||
|
'∑': '\\sum ', '∏': '\\prod ', '√': '\\sqrt ', '∠': '\\angle ',
|
||||||
|
'⊥': '\\perp ', '∥': '\\parallel ',
|
||||||
|
'→': '\\to ', '⇒': '\\Rightarrow ', '⇔': '\\Leftrightarrow ',
|
||||||
|
'∀': '\\forall ', '∃': '\\exists ', '¬': '\\neg ',
|
||||||
|
'∧': '\\land ', '∨': '\\lor ',
|
||||||
|
};
|
||||||
|
for (const [symbol, latexCmd] of Object.entries(mathSymbolMap)) {
|
||||||
|
formula = formula.split(symbol).join(latexCmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
formula = formula.replace(/\b([RNZQC])\+/g, '$1^+');
|
||||||
|
formula = formula.replace(/\b([RNZQC])\*/g, '$1^*');
|
||||||
|
formula = formula
|
||||||
|
.replace(/\\overset\s*\{\s*\\rightarrow\s*\}\s*\{([^}]+)\}/g, '\\vec{$1}')
|
||||||
|
.replace(/\\text\s*\{\s*π\s*\}/g, '\\pi')
|
||||||
|
.replace(/\\text\s*\{\s*sin\s*\}/gi, '\\sin')
|
||||||
|
.replace(/\\text\s*\{\s*cos\s*\}/gi, '\\cos')
|
||||||
|
.replace(/\\text\s*\{\s*tan\s*\}/gi, '\\tan')
|
||||||
|
.replace(/\\text\s*\{\s*log\s*\}/gi, '\\log')
|
||||||
|
.replace(/π/g, '\\pi');
|
||||||
|
|
||||||
|
const encoded = encodeURIComponent(formula);
|
||||||
|
return `${LATEX_RENDER_URL}\\inline&space;\\dpi{200}&space;${encoded}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSimpleFormula = (formula: string): boolean => {
|
||||||
|
const complexPatterns = [
|
||||||
|
/\\frac\b/, /\\sqrt\b/, /\\sum\b/, /\\int\b/, /\\prod\b/, /\\lim\b/,
|
||||||
|
/\\vec\b/, /\\overline\b/, /\\underline\b/, /\\overset\b/, /\\underset\b/,
|
||||||
|
/\\binom\b/, /\\matrix\b/, /\\begin\b/,
|
||||||
|
/\^{[^}]+}/, /_{[^}]+}/,
|
||||||
|
];
|
||||||
|
return !complexPatterns.some((pattern) => pattern.test(formula));
|
||||||
|
};
|
||||||
|
|
||||||
|
const simpleFormulaToHtml = (formula: string): string => {
|
||||||
|
let text = formula;
|
||||||
|
text = text.replace(/\\text\s*\{([^}]*)\}/g, '$1');
|
||||||
|
|
||||||
|
const greekMap: Record<string, string> = {
|
||||||
|
'\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ',
|
||||||
|
'\\epsilon': 'ε', '\\varepsilon': 'ε', '\\theta': 'θ', '\\lambda': 'λ',
|
||||||
|
'\\mu': 'μ', '\\pi': 'π', '\\sigma': 'σ', '\\phi': 'φ', '\\varphi': 'φ',
|
||||||
|
'\\omega': 'ω', '\\Delta': 'Δ', '\\Sigma': 'Σ', '\\Omega': 'Ω',
|
||||||
|
};
|
||||||
|
for (const [latex, symbol] of Object.entries(greekMap)) {
|
||||||
|
text = text.split(latex).join(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.replace(/\\sin\b/g, 'sin');
|
||||||
|
text = text.replace(/\\cos\b/g, 'cos');
|
||||||
|
text = text.replace(/\\tan\b/g, 'tan');
|
||||||
|
text = text.replace(/\\log\b/g, 'log');
|
||||||
|
text = text.replace(/\\ln\b/g, 'ln');
|
||||||
|
text = text.replace(/\\infty\b/g, '∞');
|
||||||
|
text = text.replace(/\\pm\b/g, '±');
|
||||||
|
text = text.replace(/\\times\b/g, '×');
|
||||||
|
text = text.replace(/\\div\b/g, '÷');
|
||||||
|
text = text.replace(/\\leq\b/g, '≤');
|
||||||
|
text = text.replace(/\\geq\b/g, '≥');
|
||||||
|
text = text.replace(/\\neq\b/g, '≠');
|
||||||
|
text = text.replace(/\\in\b/g, '∈');
|
||||||
|
text = text.replace(/\\subset\b/g, '⊂');
|
||||||
|
text = text.replace(/\\cup\b/g, '∪');
|
||||||
|
text = text.replace(/\\cap\b/g, '∩');
|
||||||
|
text = text.replace(/\\emptyset\b/g, '∅');
|
||||||
|
text = text.replace(/\\cdot\b/g, '·');
|
||||||
|
text = text.replace(/\\to\b/g, '→');
|
||||||
|
|
||||||
|
const superscriptMap: Record<string, string> = {
|
||||||
|
'0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴',
|
||||||
|
'5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹',
|
||||||
|
'+': '⁺', '-': '⁻', 'n': 'ⁿ',
|
||||||
|
};
|
||||||
|
const subscriptMap: Record<string, string> = {
|
||||||
|
'0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄',
|
||||||
|
'5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉',
|
||||||
|
'+': '₊', '-': '₋', 'n': 'ₙ', 'i': 'ᵢ', 'k': 'ₖ',
|
||||||
|
};
|
||||||
|
text = text.replace(/\^([0-9n+\-])/g, (_, char) => superscriptMap[char] || `^${char}`);
|
||||||
|
text = text.replace(/_([0-9nik+\-])/g, (_, char) => subscriptMap[char] || `_${char}`);
|
||||||
|
text = text.replace(/\\left\s*/g, '');
|
||||||
|
text = text.replace(/\\right\s*/g, '');
|
||||||
|
text = text.replace(/\\/g, '');
|
||||||
|
|
||||||
|
return `<span class="formula-text" style="font-style:italic;">${text}</span>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processFormula = (html: string): string => {
|
||||||
|
if (html == null || typeof html !== 'string') return '';
|
||||||
|
if (!html) return '';
|
||||||
|
|
||||||
|
const formulaRegex = /<span\s+((?:[^<>"']+|"[^"]*"|'[^']*')*)data-w-e-type=["']formula["']((?:[^<>"']+|"[^"]*"|'[^']*')*)>[\s\S]*?<\/span>/gi;
|
||||||
|
|
||||||
|
return html.replace(formulaRegex, (match) => {
|
||||||
|
const valueMatch = match.match(/data-value=["']([^"']+)["']/);
|
||||||
|
if (!valueMatch) return match;
|
||||||
|
|
||||||
|
let formula = valueMatch[1]
|
||||||
|
.replace(/\\\\/g, '\\')
|
||||||
|
.replace(/\\"/g, '"')
|
||||||
|
.replace(/\\'/g, "'");
|
||||||
|
|
||||||
|
if (!formula.trim()) return '';
|
||||||
|
|
||||||
|
if (isSimpleFormula(formula)) {
|
||||||
|
return simpleFormulaToHtml(formula);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = latexToImageUrl(formula);
|
||||||
|
return `<img class="formula-img" src="${imageUrl}" style="height:1.4em;vertical-align:middle;" />`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isHtmlContent = (value: string) => /<[a-z][\s\S]*>/i.test(value);
|
||||||
|
|
||||||
|
export const formatQuestionHtml = (title?: string, options?: { stripImages?: boolean }) => {
|
||||||
|
if (!title) return '';
|
||||||
|
let t = String(title);
|
||||||
|
if (options?.stripImages) {
|
||||||
|
t = t.replace(/<img(?![^>]*formula-img)[^>]*\/?>/gi, '');
|
||||||
|
}
|
||||||
|
t = t.replace(/<u><\/u>/gi, '<u class="fill-blank"></u>');
|
||||||
|
t = t.replace(/http:\/\//gi, 'https://');
|
||||||
|
t = fixMathSymbolsInHtml(t);
|
||||||
|
t = processFormula(t);
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuestionStemSource = (item: Record<string, unknown>) => {
|
||||||
|
const pid = item.pidTitle ? String(item.pidTitle) : '';
|
||||||
|
const stem = String(item.stemHtml || item.titleMu || item.title || item.stem || '');
|
||||||
|
if (pid && stem) return `${pid}${stem}`;
|
||||||
|
return pid || stem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuestionAnswerSource = (item: Record<string, unknown>) =>
|
||||||
|
String(item.answerMu ?? item.answer ?? '');
|
||||||
|
|
||||||
|
const IMAGE_URL_PATTERN = /\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i;
|
||||||
|
|
||||||
|
export const isImageUrl = (url: string) => {
|
||||||
|
const value = String(url || '').trim();
|
||||||
|
if (!value) return false;
|
||||||
|
if (IMAGE_URL_PATTERN.test(value)) return true;
|
||||||
|
return /(?:cos\.|myqcloud|oss-|\/temp\/|\/upload)/i.test(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseImageUrls = (raw: unknown) => {
|
||||||
|
const urls = String(raw || '')
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.replace(/[`\s]/g, '').replace(/http:\/\//gi, 'https://').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return urls.filter(isImageUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractAnswerImages = (item: Record<string, unknown>) => {
|
||||||
|
const images = new Set<string>();
|
||||||
|
parseImageUrls(item.submitAnswerPic).forEach((url) => images.add(url));
|
||||||
|
|
||||||
|
const answer = String(item.submitAnswer || '');
|
||||||
|
const links = answer.match(/https?:\/\/[^\s,]+/gi) || [];
|
||||||
|
links.forEach((link) => {
|
||||||
|
const url = link.replace(/http:\/\//gi, 'https://').trim();
|
||||||
|
if (isImageUrl(url)) images.add(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(images);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractTitleImages = (html: string) => {
|
||||||
|
if (!html) return [] as string[];
|
||||||
|
const images: string[] = [];
|
||||||
|
const imgRegex = /<img[^>]*\s*src=["']([^"']+)["'][^>]*\/?>/gi;
|
||||||
|
let match;
|
||||||
|
while ((match = imgRegex.exec(html)) !== null) {
|
||||||
|
if (/formula-img/i.test(match[0])) continue;
|
||||||
|
const url = (match[1] || '').trim().replace(/http:\/\//gi, 'https://');
|
||||||
|
if (url) images.push(url);
|
||||||
|
}
|
||||||
|
return images;
|
||||||
|
};
|
||||||
58
src/utils/roleHome.ts
Normal file
58
src/utils/roleHome.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { getCache } from '@/utils/cache';
|
||||||
|
|
||||||
|
export type UserRole = 'student' | 'teacher' | 'parent';
|
||||||
|
|
||||||
|
const VALID_ROLES: UserRole[] = ['student', 'teacher', 'parent'];
|
||||||
|
|
||||||
|
export const ROLE_HOME_URL: Record<UserRole, string> = {
|
||||||
|
student: '/pages/index/index',
|
||||||
|
teacher: '/pages/teacher/index/index',
|
||||||
|
parent: '/pages/parent/home/index',
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNonEmptyObject = (val: unknown) =>
|
||||||
|
!!val && typeof val === 'object' && Object.keys(val as object).length > 0;
|
||||||
|
|
||||||
|
/** 读取本地已保存的账号类型 */
|
||||||
|
export const getStoredRole = (): UserRole | null => {
|
||||||
|
const role = getCache('role');
|
||||||
|
return VALID_ROLES.includes(role as UserRole) ? (role as UserRole) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 是否具备可自动进入对应首页的本地登录态 */
|
||||||
|
export const hasStoredSession = (): boolean => {
|
||||||
|
const token = getCache('token');
|
||||||
|
const role = getStoredRole();
|
||||||
|
if (!token || !role) return false;
|
||||||
|
|
||||||
|
if (role === 'teacher') {
|
||||||
|
return isNonEmptyObject(getCache('teacherInfo'));
|
||||||
|
}
|
||||||
|
return isNonEmptyObject(getCache('userInfo'));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 根据账号类型获取首页路径 */
|
||||||
|
export const getRoleHomeUrl = (role: UserRole | null = getStoredRole()) =>
|
||||||
|
(role ? ROLE_HOME_URL[role] : ROLE_HOME_URL.student);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 若本地已有 token、账号类型和用户信息,则跳转到对应角色首页
|
||||||
|
* @returns 是否已触发跳转
|
||||||
|
*/
|
||||||
|
export const redirectToRoleHomeIfNeeded = (): boolean => {
|
||||||
|
if (!hasStoredSession()) return false;
|
||||||
|
|
||||||
|
const role = getStoredRole();
|
||||||
|
if (!role) return false;
|
||||||
|
|
||||||
|
const targetUrl = getRoleHomeUrl(role);
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
const current = pages[pages.length - 1];
|
||||||
|
if (current) {
|
||||||
|
const route = `/${current.route}`;
|
||||||
|
if (route === targetUrl) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.reLaunch({ url: targetUrl });
|
||||||
|
return true;
|
||||||
|
};
|
||||||
106
src/utils/subject.ts
Normal file
106
src/utils/subject.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
export interface SubjectOption {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubjectFilterOption {
|
||||||
|
label: string;
|
||||||
|
value?: number;
|
||||||
|
subjectId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 学科
|
||||||
|
export const subject_chinese: SubjectOption = {
|
||||||
|
value: 1,
|
||||||
|
label: '语文',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_math: SubjectOption = {
|
||||||
|
value: 2,
|
||||||
|
label: '数学',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_english: SubjectOption = {
|
||||||
|
value: 3,
|
||||||
|
label: '英语',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_physics: SubjectOption = {
|
||||||
|
value: 4,
|
||||||
|
label: '物理',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_chemistry: SubjectOption = {
|
||||||
|
value: 5,
|
||||||
|
label: '化学',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_bio: SubjectOption = {
|
||||||
|
value: 6,
|
||||||
|
label: '生物学',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_politics: SubjectOption = {
|
||||||
|
value: 7,
|
||||||
|
label: '政治(道法)',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_history: SubjectOption = {
|
||||||
|
value: 8,
|
||||||
|
label: '历史',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_geo: SubjectOption = {
|
||||||
|
value: 9,
|
||||||
|
label: '地理',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_science: SubjectOption = {
|
||||||
|
value: 10,
|
||||||
|
label: '科学',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_history_society: SubjectOption = {
|
||||||
|
value: 11,
|
||||||
|
label: '历史与社会',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_info: SubjectOption = {
|
||||||
|
value: 12,
|
||||||
|
label: '信息技术',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_music: SubjectOption = {
|
||||||
|
value: 13,
|
||||||
|
label: '音乐',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subject_art: SubjectOption = {
|
||||||
|
value: 14,
|
||||||
|
label: '美术',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SUBJECT_OPTIONS: SubjectOption[] = [
|
||||||
|
subject_chinese,
|
||||||
|
subject_math,
|
||||||
|
subject_english,
|
||||||
|
subject_physics,
|
||||||
|
subject_chemistry,
|
||||||
|
subject_bio,
|
||||||
|
subject_politics,
|
||||||
|
subject_history,
|
||||||
|
subject_geo,
|
||||||
|
subject_science,
|
||||||
|
subject_history_society,
|
||||||
|
subject_info,
|
||||||
|
subject_music,
|
||||||
|
subject_art,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SUBJECT_FILTER_OPTIONS: SubjectFilterOption[] = [
|
||||||
|
{ label: '全部学科' },
|
||||||
|
...SUBJECT_OPTIONS.map((item) => ({
|
||||||
|
...item,
|
||||||
|
subjectId: String(item.value),
|
||||||
|
})),
|
||||||
|
];
|
||||||
Loading…
x
Reference in New Issue
Block a user