feat: 对接家长登录
This commit is contained in:
parent
d13f75ddc0
commit
5fd397b1ea
4
.env
4
.env
@ -1,5 +1,5 @@
|
||||
#VITE_HOST = http://hjcy.niuniuhotpot.fr/prod-api
|
||||
VITE_BASE_URL = https://ai.xuexiaole.com
|
||||
VITE_BASE_URL = http://43.136.52.196:9053
|
||||
# VITE_BASE_URL = https://ai.lingxixue.com
|
||||
|
||||
# VITE_WS_URL = ws://8.138.151.141:8888/
|
||||
VITE_WS_URL = ws://ec2-13-36-229-106.eu-west-3.compute.amazonaws.com:8888/
|
||||
|
||||
@ -23,32 +23,61 @@ export const psdLogin = (data: AccountLoginParams) =>
|
||||
});
|
||||
|
||||
export interface ParentSmsLoginParams {
|
||||
mobile: string;
|
||||
smsCode: string;
|
||||
phone: string;
|
||||
verifyCode: string;
|
||||
openid: string;
|
||||
}
|
||||
|
||||
/** 家长端 - 发送登录验证码 */
|
||||
export const sendParentLoginCode = (mobile: string) =>
|
||||
request({
|
||||
url: '/parent/sendLoginCode',
|
||||
export interface ParentMpLoginResp {
|
||||
accessToken?: string | null;
|
||||
account?: string | null;
|
||||
phone?: string | null;
|
||||
wxUser?: {
|
||||
openid?: string;
|
||||
unionId?: string;
|
||||
nickname?: string | null;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/** 家长端 - 小程序 code 登录(查 openId & 绑定关系) */
|
||||
export const parentMpLogin = (code: string) =>
|
||||
request<ParentMpLoginResp>({
|
||||
url: '/mpLogin',
|
||||
method: 'POST',
|
||||
headers: { Authorization: '' },
|
||||
silent: true,
|
||||
data: { mobile },
|
||||
data: { code },
|
||||
});
|
||||
|
||||
/** 家长端 - 手机号验证码登录 */
|
||||
/** 家长端 - 发送登录验证码(form-urlencoded 请求体,非 URL 查询参数) */
|
||||
export const sendParentLoginCode = (phone: string) =>
|
||||
request({
|
||||
url: '/sms/sendMessage',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: '',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
silent: true,
|
||||
data: `phoneNumbers=${encodeURIComponent(phone)}`,
|
||||
});
|
||||
|
||||
/** 家长端 - 手机号验证码登录(绑定微信 openid),成功时 data 为 JWT 字符串 */
|
||||
export const parentSmsLogin = (data: ParentSmsLoginParams) =>
|
||||
request<string>({
|
||||
url: '/parent/loginBySms',
|
||||
url: '/smsLogin',
|
||||
method: 'POST',
|
||||
headers: { Authorization: '' },
|
||||
headers: {
|
||||
Authorization: '',
|
||||
'X-Tenant-Id': '1966391196339204098',
|
||||
},
|
||||
data: {
|
||||
mobile: data.mobile,
|
||||
smsCode: data.smsCode,
|
||||
clientType: 'PARENT',
|
||||
simSerialNumber: 'miniProgram',
|
||||
model: 'wechat',
|
||||
bindMaOpenid: true,
|
||||
adminType: 16,
|
||||
openid: data.openid,
|
||||
phone: data.phone,
|
||||
verifyCode: data.verifyCode,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
52
src/api/parent.ts
Normal file
52
src/api/parent.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { request } from '@/utils/request';
|
||||
|
||||
export interface ParentBindChildInfo {
|
||||
account?: string;
|
||||
avatar?: string;
|
||||
id?: number;
|
||||
name?: string;
|
||||
nickname?: string;
|
||||
phone?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ParentBindChildStudent {
|
||||
account?: string;
|
||||
avatar?: string;
|
||||
gradeId?: number;
|
||||
gradeName?: string;
|
||||
id?: number;
|
||||
name?: string;
|
||||
nickname?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ParentBindChildItem {
|
||||
id?: number;
|
||||
bindFlag?: number;
|
||||
bindTime?: string;
|
||||
identityType?: number;
|
||||
child?: ParentBindChildStudent;
|
||||
parent?: ParentBindChildInfo;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface PageResult<T> {
|
||||
current?: number;
|
||||
rows?: T[];
|
||||
size?: number;
|
||||
totalPage?: number;
|
||||
totalRows?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** 家长绑定孩子列表 */
|
||||
export const getParentBindChildList = (params?: { current?: number; size?: number }) =>
|
||||
request<PageResult<ParentBindChildItem>>({
|
||||
url: '/parentBindChild/list',
|
||||
method: 'GET',
|
||||
params: {
|
||||
current: params?.current ?? 1,
|
||||
size: params?.size ?? 100,
|
||||
},
|
||||
});
|
||||
@ -50,7 +50,7 @@
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wxa5522671f30c5891",
|
||||
"appid" : "wxab166c9b5b6401ea",
|
||||
"setting" : {
|
||||
"urlCheck" : false,
|
||||
"minified" : true,
|
||||
|
||||
@ -180,7 +180,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { sendParentLoginCode } from '@/api/login';
|
||||
import { parentMpLogin, sendParentLoginCode } from '@/api/login';
|
||||
import { useUserStore } from '@/state/modules/user';
|
||||
import { useTeacherStore } from '@/state/modules/teacher';
|
||||
import { setCache, getCache } from '@/utils/cache';
|
||||
@ -216,8 +216,8 @@ const teacherStore = useTeacherStore();
|
||||
const isParentLogin = computed(() => role.value === 'parent');
|
||||
type ParentStage = 'authorize' | 'bind';
|
||||
const parentStage = ref<ParentStage>('authorize');
|
||||
|
||||
const MOCK_PARENT_WX_BOUND = false;
|
||||
/** mpLogin 返回的微信 openid,绑定手机号时传给 smsLogin */
|
||||
const parentWxOpenid = ref('');
|
||||
|
||||
const smsBtnText = computed(() => {
|
||||
if (smsCountdown.value > 0) return `${smsCountdown.value}s`;
|
||||
@ -231,6 +231,7 @@ const switchRole = (value: LoginRole) => {
|
||||
|
||||
if (value === 'parent') {
|
||||
parentStage.value = 'authorize';
|
||||
parentWxOpenid.value = '';
|
||||
phone.value = '';
|
||||
smsCode.value = '';
|
||||
smsCountdown.value = 0;
|
||||
@ -334,11 +335,6 @@ const onSendCode = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkParentBoundByWxCode = async (_code: string): Promise<{ bound: boolean }> => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
return { bound: MOCK_PARENT_WX_BOUND };
|
||||
};
|
||||
|
||||
const onParentAuthorize = async () => {
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
@ -360,16 +356,29 @@ const onParentAuthorize = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { bound } = await checkParentBoundByWxCode(code);
|
||||
if (bound) {
|
||||
setCache('role', 'parent');
|
||||
const res = await parentMpLogin(code);
|
||||
const data = res?.data || {};
|
||||
const accessToken = data.accessToken;
|
||||
|
||||
if (accessToken) {
|
||||
await userStore.parentMpLoginSuccess(accessToken);
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||
setTimeout(() => navigateAfterLogin(), 800);
|
||||
return;
|
||||
}
|
||||
|
||||
const openid = data.wxUser?.openid || '';
|
||||
if (!openid) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '获取微信 openid 失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
parentWxOpenid.value = openid;
|
||||
parentStage.value = 'bind';
|
||||
if (data.phone) {
|
||||
phone.value = data.phone;
|
||||
}
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '请先绑定手机号', icon: 'none' });
|
||||
} catch (e: any) {
|
||||
@ -412,11 +421,20 @@ const onLogin = async () => {
|
||||
uni.showToast({ title: '请输入验证码', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (!parentWxOpenid.value) {
|
||||
uni.showToast({ title: '请先完成微信授权', icon: 'none' });
|
||||
parentStage.value = 'authorize';
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
uni.showLoading({ title: '登录中...', mask: true });
|
||||
try {
|
||||
await userStore.parentLogin({ mobile, smsCode: code });
|
||||
await userStore.parentLogin({
|
||||
phone: mobile,
|
||||
verifyCode: code,
|
||||
openid: parentWxOpenid.value,
|
||||
});
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||
setTimeout(() => navigateAfterLogin(), 800);
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
<view class="account-row">
|
||||
<view class="account-avatar">家</view>
|
||||
<view class="account-main">
|
||||
<text class="phone">138****4546</text>
|
||||
<text class="current-child">当前孩子:{{ activeChild.name }} · {{ activeChild.className }}</text>
|
||||
<text class="phone">{{ displayPhone }}</text>
|
||||
<text class="current-child">当前孩子:{{ activeChild.name }} · {{ activeChild.className || '暂无年级' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</ParentCard>
|
||||
@ -14,10 +14,12 @@
|
||||
|
||||
<view class="children-card">
|
||||
<ParentCard title="孩子管理" subtitle="切换后首页与报告将默认展示该孩子">
|
||||
<view class="child-grid">
|
||||
<view v-if="loading" class="state-text">加载中...</view>
|
||||
<view v-else-if="!childrenList.length" class="state-text">暂无绑定孩子</view>
|
||||
<view v-else class="child-grid">
|
||||
<view
|
||||
v-for="(child, index) in parentChildren"
|
||||
:key="child.id"
|
||||
v-for="(child, index) in childrenList"
|
||||
:key="child.bindId || child.id"
|
||||
class="child-card"
|
||||
:class="{ active: index === activeChildIndex }"
|
||||
@click="selectChild(index)"
|
||||
@ -47,19 +49,77 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { getParentBindChildList, type ParentBindChildItem } from '@/api/parent';
|
||||
import { useUserStore } from '@/state/modules/user';
|
||||
import ParentCard from '../components/ParentCard.vue';
|
||||
import ParentPage from '../components/ParentPage.vue';
|
||||
import { parentChildren } from '../utils/data';
|
||||
|
||||
interface ChildItem {
|
||||
bindId: string;
|
||||
id: string;
|
||||
name: string;
|
||||
account: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
const loading = ref(false);
|
||||
const parentPhone = ref('');
|
||||
const childrenList = ref<ChildItem[]>([]);
|
||||
const activeChildIndex = ref(0);
|
||||
const activeChild = computed(() => parentChildren[activeChildIndex.value]);
|
||||
|
||||
const activeChild = computed(() => childrenList.value[activeChildIndex.value] || {
|
||||
name: '--',
|
||||
className: '--',
|
||||
account: '',
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
fetchChildren();
|
||||
});
|
||||
|
||||
const selectChild = (index: number) => {
|
||||
activeChildIndex.value = index;
|
||||
uni.showToast({
|
||||
title: `已切换到${parentChildren[index].name}`,
|
||||
title: `已切换到${childrenList.value[index]?.name || '孩子'}`,
|
||||
icon: 'none',
|
||||
});
|
||||
};
|
||||
@ -146,6 +206,13 @@ const handleLogout = () => {
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.state-text {
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.child-card {
|
||||
padding: 20rpx;
|
||||
border-radius: 24rpx;
|
||||
|
||||
@ -90,13 +90,27 @@ export const useUserStore = defineStore('user', () => {
|
||||
await fetchUserInfo();
|
||||
};
|
||||
|
||||
/** 家长端 - 保存登录 token */
|
||||
const saveParentSession = (accessToken: string) => {
|
||||
if (!accessToken?.trim()) {
|
||||
throw new Error('登录失败');
|
||||
}
|
||||
token.value = accessToken;
|
||||
setCache('token', accessToken);
|
||||
setCache('role', 'parent');
|
||||
};
|
||||
|
||||
/** 家长端 - 微信授权已绑定,写入 token */
|
||||
const parentMpLoginSuccess = async (accessToken: string) => {
|
||||
await clear();
|
||||
saveParentSession(accessToken);
|
||||
};
|
||||
|
||||
/** 家长端 - 手机号验证码登录 */
|
||||
const parentLogin = async (data: ParentSmsLoginParams) => {
|
||||
await clear();
|
||||
const res = await parentSmsLoginApi(data);
|
||||
token.value = res.data;
|
||||
setCache('token', res.data);
|
||||
setCache('role', 'parent');
|
||||
saveParentSession(res.data);
|
||||
};
|
||||
|
||||
/** 退出登录 */
|
||||
@ -122,6 +136,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
isLogin,
|
||||
gradeId,
|
||||
accountLogin,
|
||||
parentMpLoginSuccess,
|
||||
parentLogin,
|
||||
fetchUserInfo,
|
||||
logout,
|
||||
|
||||
@ -84,7 +84,8 @@ http.interceptors.response.use(
|
||||
return response;
|
||||
}
|
||||
|
||||
if (data.code === 200) {
|
||||
// 兼容不同后端返回规范:部分接口成功码为 0(并可能带 success=true)
|
||||
if (data.code === 200 || data.code === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user