feat: 新增解绑,绑定申请同意的功能

This commit is contained in:
阿梦 2026-06-01 14:17:54 +08:00
parent 01707e0bf5
commit 9a25d8976f
7 changed files with 324 additions and 11 deletions

View File

@ -23,6 +23,8 @@ export interface ParentBindChildStudent {
export interface ParentBindChildItem { export interface ParentBindChildItem {
id?: number; id?: number;
adminFlag?: number;
leadFlag?: number;
bindFlag?: number; bindFlag?: number;
bindTime?: string; bindTime?: string;
identityType?: number; identityType?: number;
@ -90,6 +92,14 @@ export const manualBindChild = (childAcc: string) =>
data: `childAcc=${encodeURIComponent(childAcc.trim())}`, 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 { export interface WrongKpointRatioItem {
errorCount?: number; errorCount?: number;
kpointId?: number; kpointId?: number;
@ -337,3 +347,34 @@ export const parentBindApply = (params: {
data: { childDto, deviceDto }, 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,
});

View File

@ -52,13 +52,13 @@
</ParentCard> </ParentCard>
</view> --> </view> -->
<!-- <view class="quick-grid"> <view class="quick-grid">
<ParentCard v-for="item in quickActions" :key="item.title" class="quick-card" clickable @click="handleQuickAction(item)"> <ParentCard v-for="item in quickActions" :key="item.title" class="quick-card" clickable @click="handleQuickAction(item)">
<view class="quick-icon" :class="item.tone">{{ item.title.slice(0, 1) }}</view> <view class="quick-icon" :class="item.tone">{{ item.title.slice(0, 1) }}</view>
<text class="quick-title">{{ item.title }}</text> <text class="quick-title">{{ item.title }}</text>
<text class="quick-desc">{{ item.desc }}</text> <text class="quick-desc">{{ item.desc }}</text>
</ParentCard> </ParentCard>
</view> --> </view>
</ParentPage> </ParentPage>
</template> </template>
@ -71,7 +71,7 @@ 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 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'; 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] : ''; 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -24,13 +24,46 @@
:class="{ active: index === activeChildIndex }" :class="{ active: index === activeChildIndex }"
@click="selectChild(index)" @click="selectChild(index)"
> >
<view class="child-title-row">
<text class="child-name">{{ child.name }}</text> <text class="child-name">{{ child.name }}</text>
<text v-if="child.adminFlag" class="admin-tag">管理员</text>
</view>
<text class="child-account">{{ child.account }}</text> <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> </view>
<view class="bind-actions"> <view class="bind-actions">
<view class="bind-action-btn" @click="openManualBind">手动绑定</view> <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> </view>
</ParentCard> </ParentCard>
</view> </view>
@ -66,8 +99,17 @@ 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 ManualBindChild from '../components/ManualBindChild.vue';
import { scanAndGoBind } from '../utils/scanBind'; 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 DEFAULT_AVATAR = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/logo.png';
const showBindActions = false;
const userStore = useUserStore(); const userStore = useUserStore();
const parentStore = useParentStore(); const parentStore = useParentStore();
@ -76,6 +118,8 @@ const { childrenList, activeChild, activeChildIndex, loading } = storeToRefs(par
const parentAvatarUrl = computed(() => userInfo.value.avatarUrl || DEFAULT_AVATAR); const parentAvatarUrl = computed(() => userInfo.value.avatarUrl || DEFAULT_AVATAR);
const parentPhone = ref(''); const parentPhone = ref('');
const manualBindVisible = ref(false); const manualBindVisible = ref(false);
const applyLoading = ref(false);
const bindApplyList = ref<ParentBindApplyItem[]>([]);
const displayNickname = computed(() => { const displayNickname = computed(() => {
const nick = userInfo.value.nickName; const nick = userInfo.value.nickName;
@ -88,12 +132,14 @@ const displayNickname = computed(() => {
onShow(async () => { onShow(async () => {
parentPhone.value = userInfo.value.phone || parentPhone.value; parentPhone.value = userInfo.value.phone || parentPhone.value;
if (childrenList.value.length) return;
try { try {
if (!userInfo.value.id) { if (!userInfo.value.id) {
await userStore.fetchParentUserInfo(); await userStore.fetchParentUserInfo();
} }
if (!childrenList.value.length) {
await parentStore.fetchBindChildren(userInfo.value.id); await parentStore.fetchBindChildren(userInfo.value.id);
}
await fetchBindApplies();
} catch { } catch {
/* ignore */ /* ignore */
} }
@ -121,11 +167,86 @@ const onBindSuccess = async () => {
await userStore.fetchParentUserInfo(); await userStore.fetchParentUserInfo();
} }
await parentStore.fetchBindChildren(userInfo.value.id); await parentStore.fetchBindChildren(userInfo.value.id);
await fetchBindApplies();
} catch (e: any) { } catch (e: any) {
uni.showToast({ title: e?.message || '刷新孩子列表失败', icon: 'none' }); 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 = () => { const showAbout = () => {
uni.showModal({ uni.showModal({
title: '关于灵犀学AI', title: '关于灵犀学AI',
@ -186,6 +307,7 @@ const handleLogout = () => {
} }
.children-card, .children-card,
.apply-card,
.menu-card { .menu-card {
margin-top: 40rpx; 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 { .child-name {
display: block; display: block;
color: #164e63; color: #164e63;
@ -223,6 +352,15 @@ const handleLogout = () => {
font-weight: 900; font-weight: 900;
} }
.admin-tag {
padding: 4rpx 12rpx;
border-radius: 999rpx;
background: #e0f2fe;
color: #0369a1;
font-size: 20rpx;
font-weight: 700;
}
.child-account { .child-account {
display: block; display: block;
margin-top: 4rpx; margin-top: 4rpx;
@ -230,6 +368,19 @@ const handleLogout = () => {
font-size: 22rpx; 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 { .bind-actions {
display: flex; display: flex;
gap: 16rpx; 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 { .menu-card {
padding-top: 16rpx; padding-top: 16rpx;
padding-bottom: 16rpx; padding-bottom: 16rpx;

View File

@ -386,7 +386,7 @@
</template> </template>
</view> </view>
<view class="bottom-actions"> <view v-if="showDownloadReport" class="bottom-actions">
<button class="download-btn" @click="downloadReport">下载报告</button> <button class="download-btn" @click="downloadReport">下载报告</button>
<!-- <view class="home-btn" @click="goHome"> <!-- <view class="home-btn" @click="goHome">
<text class="home-roof"></text> <text class="home-roof"></text>
@ -435,6 +435,7 @@ const knowledgeTabs: { key: KnowledgeLevel; label: string }[] = [
]; ];
const REPORT_FLAG = 1; const REPORT_FLAG = 1;
const showDownloadReport = false;
const parentStore = useParentStore(); const parentStore = useParentStore();
const { activeChild } = storeToRefs(parentStore); const { activeChild } = storeToRefs(parentStore);

View File

@ -184,7 +184,7 @@ export const gradeBands: GradeBand[] = [
export const quickActions: QuickAction[] = [ export const quickActions: QuickAction[] = [
{ title: '学情追踪', desc: '查看本周学习变化', tone: 'cyan' }, { title: '学情追踪', desc: '查看本周学习变化', tone: 'cyan' },
{ title: '错题练习', desc: '按薄弱点再练一遍', tone: 'orange' }, { title: '错题练习', desc: '按薄弱点再练一遍', tone: 'orange' },
{ title: '课程学习', desc: '进入课程任务', tone: 'green' }, // { title: '课程学习', desc: '进入课程任务', tone: 'green' },
]; ];
export const reportList: ReportItem[] = [ export const reportList: ReportItem[] = [

View File

@ -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_TEACHER },
{ name: '其他', icon: `${OSS_BASE}/qita.png`, id: PARENT_TYPE_OTHER }, { 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]: '其他',
};

View File

@ -11,6 +11,9 @@ export interface ParentChildItem {
className: string; className: string;
school: string; school: string;
examNo: string; examNo: string;
parentId: string;
adminFlag: number;
isSelfBind: boolean;
} }
const CACHE_ACTIVE_CHILD_ID = 'parentActiveChildId'; const CACHE_ACTIVE_CHILD_ID = 'parentActiveChildId';
@ -23,10 +26,14 @@ const emptyChild = (): ParentChildItem => ({
className: '--', className: '--',
school: '--', school: '--',
examNo: '--', examNo: '--',
parentId: '',
adminFlag: 0,
isSelfBind: false,
}); });
const mapBindRow = (row: ParentBindChildItem): ParentChildItem => { const mapBindRow = (row: ParentBindChildItem, selfParentId: string): ParentChildItem => {
const account = row.child?.account || ''; const account = row.child?.account || '';
const parentId = String(row.parent?.id ?? '');
return { return {
bindId: String(row.id ?? row.child?.id ?? ''), bindId: String(row.id ?? row.child?.id ?? ''),
id: String(row.child?.id ?? row.id ?? ''), id: String(row.child?.id ?? row.id ?? ''),
@ -35,6 +42,9 @@ const mapBindRow = (row: ParentBindChildItem): ParentChildItem => {
className: row.child?.gradeName || '', className: row.child?.gradeName || '',
school: '', school: '',
examNo: account, examNo: account,
parentId,
adminFlag: Number(row.adminFlag || 0),
isSelfBind: parentId !== '' && parentId === selfParentId,
}; };
}; };
@ -93,7 +103,8 @@ export const useParentStore = defineStore('parent', () => {
try { try {
const res = await getParentBindChildList({ parentId: pid }); const res = await getParentBindChildList({ parentId: pid });
const rows = res.data?.rows || []; const rows = res.data?.rows || [];
childrenList.value = rows.map(mapBindRow); const selfParentId = String(pid);
childrenList.value = rows.map((row) => mapBindRow(row, selfParentId));
ensureActiveChild(); ensureActiveChild();
return childrenList.value; return childrenList.value;
} catch (err) { } catch (err) {