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 = http://43.136.52.196:9053
|
||||||
VITE_BASE_URL = https://ai.xuexiaole.com
|
# VITE_BASE_URL = https://ai.lingxixue.com
|
||||||
|
|
||||||
# VITE_WS_URL = ws://8.138.151.141:8888/
|
# 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/
|
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 {
|
export interface ParentSmsLoginParams {
|
||||||
mobile: string;
|
phone: string;
|
||||||
smsCode: string;
|
verifyCode: string;
|
||||||
|
openid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 家长端 - 发送登录验证码 */
|
export interface ParentMpLoginResp {
|
||||||
export const sendParentLoginCode = (mobile: string) =>
|
accessToken?: string | null;
|
||||||
request({
|
account?: string | null;
|
||||||
url: '/parent/sendLoginCode',
|
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',
|
method: 'POST',
|
||||||
headers: { Authorization: '' },
|
headers: { Authorization: '' },
|
||||||
silent: true,
|
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) =>
|
export const parentSmsLogin = (data: ParentSmsLoginParams) =>
|
||||||
request<string>({
|
request<string>({
|
||||||
url: '/parent/loginBySms',
|
url: '/smsLogin',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: '' },
|
headers: {
|
||||||
|
Authorization: '',
|
||||||
|
'X-Tenant-Id': '1966391196339204098',
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
mobile: data.mobile,
|
bindMaOpenid: true,
|
||||||
smsCode: data.smsCode,
|
adminType: 16,
|
||||||
clientType: 'PARENT',
|
openid: data.openid,
|
||||||
simSerialNumber: 'miniProgram',
|
phone: data.phone,
|
||||||
model: 'wechat',
|
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" : {},
|
"quickapp" : {},
|
||||||
/* 小程序特有相关 */
|
/* 小程序特有相关 */
|
||||||
"mp-weixin" : {
|
"mp-weixin" : {
|
||||||
"appid" : "wxa5522671f30c5891",
|
"appid" : "wxab166c9b5b6401ea",
|
||||||
"setting" : {
|
"setting" : {
|
||||||
"urlCheck" : false,
|
"urlCheck" : false,
|
||||||
"minified" : true,
|
"minified" : true,
|
||||||
|
|||||||
@ -180,7 +180,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onUnmounted, ref } from 'vue';
|
import { computed, onUnmounted, ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import { sendParentLoginCode } from '@/api/login';
|
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';
|
||||||
@ -216,8 +216,8 @@ const teacherStore = useTeacherStore();
|
|||||||
const isParentLogin = computed(() => role.value === 'parent');
|
const isParentLogin = computed(() => role.value === 'parent');
|
||||||
type ParentStage = 'authorize' | 'bind';
|
type ParentStage = 'authorize' | 'bind';
|
||||||
const parentStage = ref<ParentStage>('authorize');
|
const parentStage = ref<ParentStage>('authorize');
|
||||||
|
/** mpLogin 返回的微信 openid,绑定手机号时传给 smsLogin */
|
||||||
const MOCK_PARENT_WX_BOUND = false;
|
const parentWxOpenid = ref('');
|
||||||
|
|
||||||
const smsBtnText = computed(() => {
|
const smsBtnText = computed(() => {
|
||||||
if (smsCountdown.value > 0) return `${smsCountdown.value}s`;
|
if (smsCountdown.value > 0) return `${smsCountdown.value}s`;
|
||||||
@ -231,6 +231,7 @@ const switchRole = (value: LoginRole) => {
|
|||||||
|
|
||||||
if (value === 'parent') {
|
if (value === 'parent') {
|
||||||
parentStage.value = 'authorize';
|
parentStage.value = 'authorize';
|
||||||
|
parentWxOpenid.value = '';
|
||||||
phone.value = '';
|
phone.value = '';
|
||||||
smsCode.value = '';
|
smsCode.value = '';
|
||||||
smsCountdown.value = 0;
|
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 () => {
|
const onParentAuthorize = async () => {
|
||||||
if (submitting.value) return;
|
if (submitting.value) return;
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
@ -360,16 +356,29 @@ const onParentAuthorize = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { bound } = await checkParentBoundByWxCode(code);
|
const res = await parentMpLogin(code);
|
||||||
if (bound) {
|
const data = res?.data || {};
|
||||||
setCache('role', 'parent');
|
const accessToken = data.accessToken;
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
await userStore.parentMpLoginSuccess(accessToken);
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
uni.showToast({ title: '登录成功', icon: 'success' });
|
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||||
setTimeout(() => navigateAfterLogin(), 800);
|
setTimeout(() => navigateAfterLogin(), 800);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openid = data.wxUser?.openid || '';
|
||||||
|
if (!openid) {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '获取微信 openid 失败', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parentWxOpenid.value = openid;
|
||||||
parentStage.value = 'bind';
|
parentStage.value = 'bind';
|
||||||
|
if (data.phone) {
|
||||||
|
phone.value = data.phone;
|
||||||
|
}
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
uni.showToast({ title: '请先绑定手机号', icon: 'none' });
|
uni.showToast({ title: '请先绑定手机号', icon: 'none' });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -412,11 +421,20 @@ const onLogin = async () => {
|
|||||||
uni.showToast({ title: '请输入验证码', icon: 'none' });
|
uni.showToast({ title: '请输入验证码', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!parentWxOpenid.value) {
|
||||||
|
uni.showToast({ title: '请先完成微信授权', icon: 'none' });
|
||||||
|
parentStage.value = 'authorize';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
uni.showLoading({ title: '登录中...', mask: true });
|
uni.showLoading({ title: '登录中...', mask: true });
|
||||||
try {
|
try {
|
||||||
await userStore.parentLogin({ mobile, smsCode: code });
|
await userStore.parentLogin({
|
||||||
|
phone: mobile,
|
||||||
|
verifyCode: code,
|
||||||
|
openid: parentWxOpenid.value,
|
||||||
|
});
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
uni.showToast({ title: '登录成功', icon: 'success' });
|
uni.showToast({ title: '登录成功', icon: 'success' });
|
||||||
setTimeout(() => navigateAfterLogin(), 800);
|
setTimeout(() => navigateAfterLogin(), 800);
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
<view class="account-row">
|
<view class="account-row">
|
||||||
<view class="account-avatar">家</view>
|
<view class="account-avatar">家</view>
|
||||||
<view class="account-main">
|
<view class="account-main">
|
||||||
<text class="phone">138****4546</text>
|
<text class="phone">{{ displayPhone }}</text>
|
||||||
<text class="current-child">当前孩子:{{ activeChild.name }} · {{ activeChild.className }}</text>
|
<text class="current-child">当前孩子:{{ activeChild.name }} · {{ activeChild.className || '暂无年级' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</ParentCard>
|
</ParentCard>
|
||||||
@ -14,10 +14,12 @@
|
|||||||
|
|
||||||
<view class="children-card">
|
<view class="children-card">
|
||||||
<ParentCard title="孩子管理" subtitle="切换后首页与报告将默认展示该孩子">
|
<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
|
<view
|
||||||
v-for="(child, index) in parentChildren"
|
v-for="(child, index) in childrenList"
|
||||||
:key="child.id"
|
:key="child.bindId || child.id"
|
||||||
class="child-card"
|
class="child-card"
|
||||||
:class="{ active: index === activeChildIndex }"
|
:class="{ active: index === activeChildIndex }"
|
||||||
@click="selectChild(index)"
|
@click="selectChild(index)"
|
||||||
@ -47,19 +49,77 @@
|
|||||||
|
|
||||||
<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 { getParentBindChildList, type ParentBindChildItem } from '@/api/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 { parentChildren } from '../utils/data';
|
|
||||||
|
interface ChildItem {
|
||||||
|
bindId: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
account: string;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const loading = ref(false);
|
||||||
|
const parentPhone = ref('');
|
||||||
|
const childrenList = ref<ChildItem[]>([]);
|
||||||
const activeChildIndex = ref(0);
|
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) => {
|
const selectChild = (index: number) => {
|
||||||
activeChildIndex.value = index;
|
activeChildIndex.value = index;
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: `已切换到${parentChildren[index].name}`,
|
title: `已切换到${childrenList.value[index]?.name || '孩子'}`,
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -146,6 +206,13 @@ const handleLogout = () => {
|
|||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.state-text {
|
||||||
|
padding: 24rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.child-card {
|
.child-card {
|
||||||
padding: 20rpx;
|
padding: 20rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
|
|||||||
@ -90,13 +90,27 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
await fetchUserInfo();
|
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) => {
|
const parentLogin = async (data: ParentSmsLoginParams) => {
|
||||||
await clear();
|
await clear();
|
||||||
const res = await parentSmsLoginApi(data);
|
const res = await parentSmsLoginApi(data);
|
||||||
token.value = res.data;
|
saveParentSession(res.data);
|
||||||
setCache('token', res.data);
|
|
||||||
setCache('role', 'parent');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 退出登录 */
|
/** 退出登录 */
|
||||||
@ -122,6 +136,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
isLogin,
|
isLogin,
|
||||||
gradeId,
|
gradeId,
|
||||||
accountLogin,
|
accountLogin,
|
||||||
|
parentMpLoginSuccess,
|
||||||
parentLogin,
|
parentLogin,
|
||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
logout,
|
logout,
|
||||||
|
|||||||
@ -84,7 +84,8 @@ http.interceptors.response.use(
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.code === 200) {
|
// 兼容不同后端返回规范:部分接口成功码为 0(并可能带 success=true)
|
||||||
|
if (data.code === 200 || data.code === 0) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user