feat: 新增解绑,绑定申请同意的功能
This commit is contained in:
parent
01707e0bf5
commit
9a25d8976f
@ -23,6 +23,8 @@ export interface ParentBindChildStudent {
|
||||
|
||||
export interface ParentBindChildItem {
|
||||
id?: number;
|
||||
adminFlag?: number;
|
||||
leadFlag?: number;
|
||||
bindFlag?: number;
|
||||
bindTime?: string;
|
||||
identityType?: number;
|
||||
@ -90,6 +92,14 @@ export const manualBindChild = (childAcc: string) =>
|
||||
data: `childAcc=${encodeURIComponent(childAcc.trim())}`,
|
||||
});
|
||||
|
||||
/** 解绑家长与孩子 */
|
||||
export const unbindParentChild = (data: { parentId: string | number; childId: string | number }) =>
|
||||
request({
|
||||
url: '/parentBindChild/unBound',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
|
||||
export interface WrongKpointRatioItem {
|
||||
errorCount?: number;
|
||||
kpointId?: number;
|
||||
@ -337,3 +347,34 @@ export const parentBindApply = (params: {
|
||||
data: { childDto, deviceDto },
|
||||
});
|
||||
};
|
||||
|
||||
export interface ParentBindApplyItem {
|
||||
id?: string | number;
|
||||
createTime?: string;
|
||||
initiatingUserPhone?: string;
|
||||
initiatingUserAvatar?: string;
|
||||
childUserName?: string;
|
||||
childUserAvatar?: string;
|
||||
simSerialNumber?: string;
|
||||
identityType?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 当前家长待处理绑定申请 */
|
||||
export const getParentBindApplies = () =>
|
||||
request<ParentBindApplyItem[]>({
|
||||
url: '/parentBind/applyInfoList',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
/** 管理员审批绑定申请 */
|
||||
export const approveParentBindApply = (data: {
|
||||
id: string | number;
|
||||
approvalFlag: boolean;
|
||||
leadFlag?: number;
|
||||
}) =>
|
||||
request({
|
||||
url: '/parentBind/admin/approval',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
|
||||
@ -52,13 +52,13 @@
|
||||
</ParentCard>
|
||||
</view> -->
|
||||
|
||||
<!-- <view class="quick-grid">
|
||||
<view class="quick-grid">
|
||||
<ParentCard v-for="item in quickActions" :key="item.title" class="quick-card" clickable @click="handleQuickAction(item)">
|
||||
<view class="quick-icon" :class="item.tone">{{ item.title.slice(0, 1) }}</view>
|
||||
<text class="quick-title">{{ item.title }}</text>
|
||||
<text class="quick-desc">{{ item.desc }}</text>
|
||||
</ParentCard>
|
||||
</view> -->
|
||||
</view>
|
||||
</ParentPage>
|
||||
</template>
|
||||
|
||||
@ -71,7 +71,7 @@ import { useParentStore } from '@/state/modules/parent';
|
||||
import { useUserStore } from '@/state/modules/user';
|
||||
import ParentCard from '../components/ParentCard.vue';
|
||||
import ParentPage from '../components/ParentPage.vue';
|
||||
import type { GradeBand, SubjectTrend } from '../utils/data';
|
||||
import { quickActions, type GradeBand, type QuickAction, type SubjectTrend } from '../utils/data';
|
||||
|
||||
const DEFAULT_AVATAR = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/logo.png';
|
||||
|
||||
@ -175,6 +175,25 @@ const trendLabel = (item: SubjectTrend) => {
|
||||
};
|
||||
return item.status ? map[item.status] : '';
|
||||
};
|
||||
|
||||
const showComingSoon = (name: string) => {
|
||||
uni.showToast({
|
||||
title: `${name}建设中`,
|
||||
icon: 'none',
|
||||
});
|
||||
};
|
||||
|
||||
const handleQuickAction = (item: QuickAction) => {
|
||||
if (item.title === '学情追踪') {
|
||||
uni.navigateTo({ url: '/pages/parent/analysis/index' });
|
||||
return;
|
||||
}
|
||||
if (item.title === '错题练习') {
|
||||
uni.navigateTo({ url: '/pages/wrong/index' });
|
||||
return;
|
||||
}
|
||||
showComingSoon(item.title);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -24,13 +24,46 @@
|
||||
:class="{ active: index === activeChildIndex }"
|
||||
@click="selectChild(index)"
|
||||
>
|
||||
<view class="child-title-row">
|
||||
<text class="child-name">{{ child.name }}</text>
|
||||
<text v-if="child.adminFlag" class="admin-tag">管理员</text>
|
||||
</view>
|
||||
<text class="child-account">{{ child.account }}</text>
|
||||
<view class="child-line">{{ child.className || '暂无年级' }}</view>
|
||||
<view v-if="canUnbind(child)" class="child-unbind" @click.stop="onUnbind(child)">
|
||||
解绑
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bind-actions">
|
||||
<view class="bind-action-btn" @click="openManualBind">手动绑定</view>
|
||||
<view class="bind-action-btn primary" @click="onScanBind">扫码绑定</view>
|
||||
<!-- <view class="bind-action-btn primary" @click="onScanBind">扫码绑定</view> -->
|
||||
</view>
|
||||
</ParentCard>
|
||||
</view>
|
||||
|
||||
<view class="apply-card">
|
||||
<ParentCard title="绑定申请" :subtitle="`待处理 ${bindApplyList.length} 条`">
|
||||
<view v-if="applyLoading" class="state-text">加载中...</view>
|
||||
<view v-else-if="!bindApplyList.length" class="state-text">暂无待处理申请</view>
|
||||
<view v-else class="apply-list">
|
||||
<view v-for="item in bindApplyList" :key="item.id" class="apply-item">
|
||||
<view class="apply-user">
|
||||
<image class="apply-avatar" :src="item.initiatingUserAvatar || parentAvatarUrl" mode="aspectFill" />
|
||||
<view class="apply-user-main">
|
||||
<text class="apply-name">{{ item.initiatingUserPhone || '未知用户' }}</text>
|
||||
<text class="apply-time">申请于 {{ item.createTime || '--' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="apply-target">
|
||||
<text v-if="item.simSerialNumber">申请绑定设备:{{ item.simSerialNumber }}</text>
|
||||
<text v-else>申请绑定孩子:{{ item.childUserName || '--' }}({{ identityText(item.identityType) }})</text>
|
||||
</view>
|
||||
<view class="apply-actions">
|
||||
<view class="apply-btn agree" @click="dealApply(item, true)">通过</view>
|
||||
<view class="apply-btn reject" @click="dealApply(item, false)">拒绝</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</ParentCard>
|
||||
</view>
|
||||
@ -66,8 +99,17 @@ import ParentCard from '../components/ParentCard.vue';
|
||||
import ParentPage from '../components/ParentPage.vue';
|
||||
import ManualBindChild from '../components/ManualBindChild.vue';
|
||||
import { scanAndGoBind } from '../utils/scanBind';
|
||||
import {
|
||||
approveParentBindApply,
|
||||
getParentBindApplies,
|
||||
unbindParentChild,
|
||||
type ParentBindApplyItem,
|
||||
} from '@/api/parent';
|
||||
import { PARENT_TYPE_MAP } from '../utils/parentIdentity';
|
||||
import type { ParentChildItem } from '@/state/modules/parent';
|
||||
|
||||
const DEFAULT_AVATAR = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/logo.png';
|
||||
const showBindActions = false;
|
||||
|
||||
const userStore = useUserStore();
|
||||
const parentStore = useParentStore();
|
||||
@ -76,6 +118,8 @@ const { childrenList, activeChild, activeChildIndex, loading } = storeToRefs(par
|
||||
const parentAvatarUrl = computed(() => userInfo.value.avatarUrl || DEFAULT_AVATAR);
|
||||
const parentPhone = ref('');
|
||||
const manualBindVisible = ref(false);
|
||||
const applyLoading = ref(false);
|
||||
const bindApplyList = ref<ParentBindApplyItem[]>([]);
|
||||
|
||||
const displayNickname = computed(() => {
|
||||
const nick = userInfo.value.nickName;
|
||||
@ -88,12 +132,14 @@ const displayNickname = computed(() => {
|
||||
|
||||
onShow(async () => {
|
||||
parentPhone.value = userInfo.value.phone || parentPhone.value;
|
||||
if (childrenList.value.length) return;
|
||||
try {
|
||||
if (!userInfo.value.id) {
|
||||
await userStore.fetchParentUserInfo();
|
||||
}
|
||||
if (!childrenList.value.length) {
|
||||
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||
}
|
||||
await fetchBindApplies();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@ -121,11 +167,86 @@ const onBindSuccess = async () => {
|
||||
await userStore.fetchParentUserInfo();
|
||||
}
|
||||
await parentStore.fetchBindChildren(userInfo.value.id);
|
||||
await fetchBindApplies();
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message || '刷新孩子列表失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
const canUnbind = (child: ParentChildItem) => {
|
||||
if (!childrenList.value.length) return false;
|
||||
return !!child.isSelfBind || !!child.adminFlag;
|
||||
};
|
||||
|
||||
const onUnbind = (child: ParentChildItem) => {
|
||||
const parentId = userInfo.value.id;
|
||||
if (!parentId || !child.id) return;
|
||||
uni.showModal({
|
||||
title: '解绑确认',
|
||||
content: `确定解绑「${child.name}」吗?`,
|
||||
confirmText: '解绑',
|
||||
confirmColor: '#ef4444',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return;
|
||||
try {
|
||||
await unbindParentChild({
|
||||
parentId,
|
||||
childId: child.id,
|
||||
});
|
||||
uni.showToast({ title: '解绑成功', icon: 'success' });
|
||||
await parentStore.fetchBindChildren(parentId);
|
||||
await fetchBindApplies();
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message || '解绑失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const identityText = (identityType?: number) => {
|
||||
if (!identityType) return '其他';
|
||||
return PARENT_TYPE_MAP[identityType] || '其他';
|
||||
};
|
||||
|
||||
const fetchBindApplies = async () => {
|
||||
applyLoading.value = true;
|
||||
try {
|
||||
const res = await getParentBindApplies();
|
||||
bindApplyList.value = res.data || [];
|
||||
} catch (e: any) {
|
||||
bindApplyList.value = [];
|
||||
uni.showToast({ title: e?.message || '绑定申请加载失败', icon: 'none' });
|
||||
} finally {
|
||||
applyLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const dealApply = (item: ParentBindApplyItem, approvalFlag: boolean) => {
|
||||
const action = approvalFlag ? '通过' : '拒绝';
|
||||
const target = item.simSerialNumber || item.childUserName || '该请求';
|
||||
uni.showModal({
|
||||
title: `${action}申请`,
|
||||
content: `确定${action}绑定「${target}」的申请吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return;
|
||||
try {
|
||||
await approveParentBindApply({
|
||||
id: String(item.id || ''),
|
||||
approvalFlag,
|
||||
leadFlag: approvalFlag ? 0 : undefined,
|
||||
});
|
||||
uni.showToast({ title: `${action}成功`, icon: 'success' });
|
||||
await Promise.all([
|
||||
parentStore.fetchBindChildren(userInfo.value.id),
|
||||
fetchBindApplies(),
|
||||
]);
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e?.message || `${action}失败`, icon: 'none' });
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showAbout = () => {
|
||||
uni.showModal({
|
||||
title: '关于灵犀学AI',
|
||||
@ -186,6 +307,7 @@ const handleLogout = () => {
|
||||
}
|
||||
|
||||
.children-card,
|
||||
.apply-card,
|
||||
.menu-card {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
@ -216,6 +338,13 @@ const handleLogout = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.child-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.child-name {
|
||||
display: block;
|
||||
color: #164e63;
|
||||
@ -223,6 +352,15 @@ const handleLogout = () => {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.admin-tag {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.child-account {
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
@ -230,6 +368,19 @@ const handleLogout = () => {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.child-line {
|
||||
margin-top: 8rpx;
|
||||
color: #64748b;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.child-unbind {
|
||||
margin-top: 12rpx;
|
||||
color: #ef4444;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bind-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
@ -257,6 +408,85 @@ const handleLogout = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.apply-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.apply-item {
|
||||
padding: 20rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.apply-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.apply-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #ecfeff;
|
||||
}
|
||||
|
||||
.apply-user-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.apply-name {
|
||||
display: block;
|
||||
color: #164e63;
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.apply-time {
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
color: #94a3b8;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.apply-target {
|
||||
margin-top: 14rpx;
|
||||
color: #334155;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.apply-actions {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
|
||||
&.agree {
|
||||
background: linear-gradient(135deg, #0891b2, #06b6d4);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.reject {
|
||||
border: 2rpx solid #e2e8f0;
|
||||
color: #64748b;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
padding-top: 16rpx;
|
||||
padding-bottom: 16rpx;
|
||||
|
||||
@ -386,7 +386,7 @@
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="bottom-actions">
|
||||
<view v-if="showDownloadReport" class="bottom-actions">
|
||||
<button class="download-btn" @click="downloadReport">下载报告</button>
|
||||
<!-- <view class="home-btn" @click="goHome">
|
||||
<text class="home-roof"></text>
|
||||
@ -435,6 +435,7 @@ const knowledgeTabs: { key: KnowledgeLevel; label: string }[] = [
|
||||
];
|
||||
|
||||
const REPORT_FLAG = 1;
|
||||
const showDownloadReport = false;
|
||||
|
||||
const parentStore = useParentStore();
|
||||
const { activeChild } = storeToRefs(parentStore);
|
||||
|
||||
@ -184,7 +184,7 @@ export const gradeBands: GradeBand[] = [
|
||||
export const quickActions: QuickAction[] = [
|
||||
{ title: '学情追踪', desc: '查看本周学习变化', tone: 'cyan' },
|
||||
{ title: '错题练习', desc: '按薄弱点再练一遍', tone: 'orange' },
|
||||
{ title: '课程学习', desc: '进入课程任务', tone: 'green' },
|
||||
// { title: '课程学习', desc: '进入课程任务', tone: 'green' },
|
||||
];
|
||||
|
||||
export const reportList: ReportItem[] = [
|
||||
|
||||
@ -26,3 +26,14 @@ export const PARENT_IDENTITY_LIST: ParentIdentityOption[] = [
|
||||
{ name: '导学师', icon: `${OSS_BASE}/qita.png`, id: PARENT_TYPE_TEACHER },
|
||||
{ name: '其他', icon: `${OSS_BASE}/qita.png`, id: PARENT_TYPE_OTHER },
|
||||
];
|
||||
|
||||
export const PARENT_TYPE_MAP: Record<number, string> = {
|
||||
[PARENT_TYPE_FATHER]: '爸爸',
|
||||
[PARENT_TYPE_MOTHER]: '妈妈',
|
||||
[PARENT_TYPE_GRANDFATHER]: '爷爷',
|
||||
[PARENT_TYPE_GRANDMOTHER]: '奶奶',
|
||||
[PARENT_TYPE_GRANDFATHER2]: '外公',
|
||||
[PARENT_TYPE_GRANDMOTHER2]: '外婆',
|
||||
[PARENT_TYPE_TEACHER]: '导学师',
|
||||
[PARENT_TYPE_OTHER]: '其他',
|
||||
};
|
||||
|
||||
@ -11,6 +11,9 @@ export interface ParentChildItem {
|
||||
className: string;
|
||||
school: string;
|
||||
examNo: string;
|
||||
parentId: string;
|
||||
adminFlag: number;
|
||||
isSelfBind: boolean;
|
||||
}
|
||||
|
||||
const CACHE_ACTIVE_CHILD_ID = 'parentActiveChildId';
|
||||
@ -23,10 +26,14 @@ const emptyChild = (): ParentChildItem => ({
|
||||
className: '--',
|
||||
school: '--',
|
||||
examNo: '--',
|
||||
parentId: '',
|
||||
adminFlag: 0,
|
||||
isSelfBind: false,
|
||||
});
|
||||
|
||||
const mapBindRow = (row: ParentBindChildItem): ParentChildItem => {
|
||||
const mapBindRow = (row: ParentBindChildItem, selfParentId: string): ParentChildItem => {
|
||||
const account = row.child?.account || '';
|
||||
const parentId = String(row.parent?.id ?? '');
|
||||
return {
|
||||
bindId: String(row.id ?? row.child?.id ?? ''),
|
||||
id: String(row.child?.id ?? row.id ?? ''),
|
||||
@ -35,6 +42,9 @@ const mapBindRow = (row: ParentBindChildItem): ParentChildItem => {
|
||||
className: row.child?.gradeName || '',
|
||||
school: '',
|
||||
examNo: account,
|
||||
parentId,
|
||||
adminFlag: Number(row.adminFlag || 0),
|
||||
isSelfBind: parentId !== '' && parentId === selfParentId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -93,7 +103,8 @@ export const useParentStore = defineStore('parent', () => {
|
||||
try {
|
||||
const res = await getParentBindChildList({ parentId: pid });
|
||||
const rows = res.data?.rows || [];
|
||||
childrenList.value = rows.map(mapBindRow);
|
||||
const selfParentId = String(pid);
|
||||
childrenList.value = rows.map((row) => mapBindRow(row, selfParentId));
|
||||
ensureActiveChild();
|
||||
return childrenList.value;
|
||||
} catch (err) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user