wip: 临时提交

This commit is contained in:
Ray.Hao
2025-05-29 17:52:37 +08:00
parent 220f719c38
commit ad4be7a197
10 changed files with 219 additions and 317 deletions

View File

@@ -38,39 +38,26 @@ const AuthAPI = {
},
/**
* 微信登录 (基础版)
* 微信小程序授权登录 (仅使用code获取OpenID)
* @param code 微信登录凭证
* @returns 登录结果
*/
wechatLogin(code: string): Promise<LoginResult> {
return request<LoginResult>({
url: `${AUTH_BASE_URL}/wechat/login`,
url: `${AUTH_BASE_URL}/wx/miniapp/code-login`,
method: "POST",
data: { code },
});
},
/**
* 微信小程序增强登录 (获取手机号)
* @param data 包含code, encryptedData, iv等的登录数据
* @returns 登录结果
*/
wechatMiniLogin(data: WxLoginData): Promise<LoginResult> {
return request<LoginResult>({
url: `${AUTH_BASE_URL}/wechat/mini-login`,
method: "POST",
data,
});
},
/**
* 微信一键登录 (通过手机号)
* @param data 包含code和phoneCode的登录数据
* 微信小程序手机号授权登录
* @param data 包含codeencryptedDataiv等手机号相关数据
* @returns 登录结果
*/
wechatPhoneLogin(data: WxLoginData): Promise<LoginResult> {
return request<LoginResult>({
url: `${AUTH_BASE_URL}/wechat/phone-login`,
url: `${AUTH_BASE_URL}/wx/miniapp/phone-login`,
method: "POST",
data,
});

View File

@@ -0,0 +1,159 @@
/**
* 微信授权服务
* 处理微信登录授权、获取用户信息等功能
*/
import { ref } from "vue";
import { getAccessToken } from "@/utils/auth";
export function useWechat() {
// 定义微信授权状态
const authState = ref({
isLogining: false,
authDenied: false,
});
/**
* 获取微信登录凭证code
* @returns Promise 返回登录凭证code
*/
const getLoginCode = (): Promise<string> => {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.login({
provider: "weixin",
success: (res) => {
if (res.code) {
resolve(res.code);
} else {
reject(new Error("获取微信登录凭证失败"));
}
},
fail: (err) => {
reject(err);
},
});
// #endif
// #ifndef MP-WEIXIN
reject(new Error("当前环境不支持微信登录"));
// #endif
});
};
/**
* 获取微信用户手机号
* @param e 微信授权返回的事件对象
* @returns Promise 返回包含手机号加密数据的对象
*/
const getPhoneNumber = (
e: any
): Promise<{ code: string; encryptedData?: string; iv?: string }> => {
return new Promise((resolve, reject) => {
authState.value.isLogining = true;
// 判断授权是否成功
if (e.detail.errMsg !== "getPhoneNumber:ok") {
authState.value.isLogining = false;
authState.value.authDenied = true;
reject(new Error("用户拒绝授权"));
return;
}
// 获取登录凭证code
getLoginCode()
.then((code) => {
// 在微信小程序环境下可以获取encryptedData和iv
// #ifdef MP-WEIXIN
resolve({
code,
encryptedData: e.detail.encryptedData,
iv: e.detail.iv,
});
// #endif
// 其他环境或新版本接口
// #ifndef MP-WEIXIN
resolve({
code,
// 新版本接口在e.detail.code中包含手机号获取凭证
...(e.detail.code ? { phoneCode: e.detail.code } : {}),
});
// #endif
})
.catch((err) => {
reject(err);
})
.finally(() => {
authState.value.isLogining = false;
});
});
};
/**
* 检查会话有效性
* @returns Promise 返回会话是否有效
*/
const checkSession = (): Promise<boolean> => {
return new Promise((resolve) => {
const token = getAccessToken();
if (!token) {
resolve(false);
return;
}
// 调用后端接口验证token有效性
uni.request({
url: "/api/v1/auth/check-session",
method: "GET",
header: {
Authorization: `Bearer ${token}`,
},
success: (res: any) => {
if (res.statusCode === 200 && res.data.valid) {
resolve(true);
} else {
resolve(false);
}
},
fail: () => {
resolve(false);
},
});
});
};
/**
* 获取用户头像昵称
* 注意此接口已于2021年弃用仅作为兼容保留
* 推荐使用button组件的open-type="chooseAvatar"让用户选择头像
*/
const getUserProfile = (): Promise<any> => {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.getUserProfile({
desc: "用于完善用户资料",
success: (res) => {
resolve(res.userInfo);
},
fail: (err) => {
reject(err);
},
});
// #endif
// #ifndef MP-WEIXIN
reject(new Error("当前环境不支持获取用户信息"));
// #endif
});
};
return {
authState,
getLoginCode,
getPhoneNumber,
checkSession,
getUserProfile,
};
}

View File

@@ -6,11 +6,6 @@
"style": {},
"layout": "tabbar"
},
{
"path": "pages/login/complete-profile",
"type": "page",
"style": {}
},
{
"path": "pages/login/index",
"type": "page",
@@ -37,6 +32,11 @@
"type": "page",
"style": {}
},
{
"path": "pages/mine/profile/complete-profile",
"type": "page",
"style": {}
},
{
"path": "pages/mine/profile/index",
"type": "page",

View File

@@ -3,48 +3,43 @@
<!-- 背景图 -->
<image src="/static/images/login-bg.svg" mode="aspectFill" class="login-bg" />
<!-- Logo和标题区域 -->
<view class="header">
<image src="/static/images/logo.png" mode="aspectFit" class="logo" />
<text class="title">您好欢迎回来</text>
<text class="subtitle">登录您的账号开始愉快的旅程</text>
<image src="/static/logo.png" class="logo" />
<text class="title">有来开源</text>
<text class="subtitle">专注于构建高效开发的应用解决方案</text>
</view>
<view class="login-card">
<view class="form-wrap">
<!-- 账号密码登录表单 -->
<wd-form :model="LoginData" v-if="loginType === 'account'" ref="loginFormRef">
<wd-form v-if="loginType === 'account'" ref="loginFormRef" :model="loginFormData">
<!-- 用户名输入框 -->
<view class="form-item">
<wd-icon name="user" size="20" class="input-icon" />
<wd-icon name="user" size="22" class="input-icon" />
<input
v-model="LoginData.username"
v-model="loginFormData.username"
class="form-input"
placeholder="请输入用户名"
placeholder-class="input-placeholder"
/>
<wd-icon
v-if="LoginData.username"
name="error-fill"
size="14"
class="clear-icon"
@click="LoginData.username = ''"
/>
</view>
<view class="divider"></view>
<!-- 密码输入框 -->
<view class="form-item">
<wd-icon name="lock" size="20" class="input-icon" />
<wd-icon name="lock-on" size="22" class="input-icon" />
<input
v-model="LoginData.password"
v-model="loginFormData.password"
class="form-input"
:type="showPassword ? 'text' : 'password'"
placeholder="请输入密码"
placeholder-class="input-placeholder"
/>
<wd-icon
:name="showPassword ? 'view' : 'view-off'"
size="14"
:name="showPassword ? 'eye-open' : 'eye-close'"
size="18"
color="#9ca3af"
class="eye-icon"
@click="showPassword = !showPassword"
/>
@@ -124,8 +119,7 @@ import { onLoad } from "@dcloudio/uni-app";
import { type LoginData } from "@/api/auth";
import { useUserStore } from "@/store/modules/user.store";
import { useToast } from "wot-design-uni";
import { getWxLoginCode, getWxPhoneNumber, wxAuthState } from "@/services/wechat.service";
import { useTheme } from "@/composables/useTheme";
import { useWechat } from "@/composables/useWechat";
const loginFormRef = ref();
const toast = useToast();
@@ -133,58 +127,32 @@ const loading = ref(false);
const userStore = useUserStore();
const showPassword = ref(false);
const loginType = ref<"account" | "phone">("account");
const { theme } = useTheme();
const { authState, getLoginCode, getPhoneNumber } = useWechat();
// 登录表单数据
const LoginData = ref<LoginData>({
const loginFormData = ref<LoginData>({
username: "",
password: "",
});
// 获取重定向参数
const redirect = ref("");
const redirect = ref("/pages/index/index");
onLoad((options) => {
if (options) {
redirect.value = options.redirect ? decodeURIComponent(options.redirect) : "/pages/index/index";
} else {
redirect.value = "/pages/index/index";
if (options && options.redirect) {
redirect.value = decodeURIComponent(options.redirect);
}
// 检查是否已登录
checkLoginStatus();
});
// 检查登录状态
const checkLoginStatus = async () => {
try {
const token = uni.getStorageSync("app_token");
if (token) {
// 验证token有效性
const isValid = await userStore.checkSession();
if (isValid) {
// 已登录,获取用户信息
await userStore.getInfo();
// 重定向到首页或指定页面
setTimeout(() => {
uni.reLaunch({ url: redirect.value });
}, 100);
}
}
} catch (error) {
console.error("检查登录状态失败", error);
}
};
// 账号密码登录处理
const handleAccountLogin = () => {
if (loading.value) return;
// 表单验证
if (!LoginData.value.username) {
if (!loginFormData.value.username) {
toast.error("请输入用户名");
return;
}
if (!LoginData.value.password) {
if (!loginFormData.value.password) {
toast.error("请输入密码");
return;
}
@@ -192,27 +160,17 @@ const handleAccountLogin = () => {
loading.value = true;
userStore
.login(LoginData.value)
.login(loginFormData.value)
.then(() => userStore.getInfo())
.then(() => {
toast.success("登录成功");
// 检查用户信息是否完整
if (!userStore.isUserInfoComplete()) {
// 信息不完整,跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
// 否则直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
}, 1000);
}
// 账号密码登录直接跳转到重定向页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
});
}, 1000);
})
.catch((error) => {
toast.error(error?.message || "登录失败");
@@ -223,16 +181,16 @@ const handleAccountLogin = () => {
};
// 微信一键登录(通过手机号)
const handleWechatPhoneLogin = async (e) => {
if (loading.value || wxAuthState.value.isLogining) return;
const handleWechatPhoneLogin = async (e: any) => {
if (loading.value || authState.value.isLogining) return;
loading.value = true;
try {
// 获取手机号加密数据
const phoneData = await getWxPhoneNumber(e);
const phoneData = await getPhoneNumber(e);
// 调用登录接口
const result = await userStore.loginByWechatPhone(phoneData);
const result: any = await userStore.loginWithWxPhone(phoneData);
// 获取用户信息
await userStore.getInfo();
@@ -243,7 +201,7 @@ const handleWechatPhoneLogin = async (e) => {
// 跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
@@ -254,7 +212,7 @@ const handleWechatPhoneLogin = async (e) => {
});
}, 1000);
}
} catch (error) {
} catch (error: any) {
if (error.message === "用户拒绝授权") {
toast.error("您已拒绝授权获取手机号");
} else {
@@ -266,7 +224,7 @@ const handleWechatPhoneLogin = async (e) => {
}
};
// 微信登录处理
// 微信授权登录处理
const handleWechatLogin = async () => {
if (loading.value) return;
loading.value = true;
@@ -274,10 +232,10 @@ const handleWechatLogin = async () => {
try {
// #ifdef MP-WEIXIN
// 获取微信登录的临时 code
const code = await getWxLoginCode();
const code = await getLoginCode();
// 尝试使用微信登录接口
const result = await userStore.loginByWechat(code);
// 尝试使用微信授权登录接口
const result: any = await userStore.loginWithWxCode(code);
// 获取用户信息
await userStore.getInfo();
@@ -288,7 +246,7 @@ const handleWechatLogin = async () => {
// 如果信息不完整,跳转到完善信息页面
setTimeout(() => {
uni.navigateTo({
url: `/pages/login/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
url: `/pages/mine/profile/complete-profile?redirect=${encodeURIComponent(redirect.value)}`,
});
}, 1000);
} else {
@@ -304,7 +262,7 @@ const handleWechatLogin = async () => {
// #ifndef MP-WEIXIN
toast.error("当前环境不支持微信登录");
// #endif
} catch (error) {
} catch (error: any) {
toast.error(error?.message || "微信登录失败");
} finally {
loading.value = false;

View File

@@ -1,148 +0,0 @@
/**
* 微信授权服务
* 处理微信登录授权、获取用户信息等功能
*/
import { ref } from "vue";
import { getAccessToken } from "@/utils/auth";
// 定义微信授权状态
export const wxAuthState = ref({
isLogining: false,
authDenied: false,
});
/**
* 获取微信登录凭证
* @returns Promise 返回登录凭证code
*/
export function getWxLoginCode(): Promise<string> {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.login({
provider: "weixin",
success: (res) => {
if (res.code) {
resolve(res.code);
} else {
reject(new Error("获取微信登录凭证失败"));
}
},
fail: (err) => {
reject(err);
},
});
// #endif
// #ifndef MP-WEIXIN
reject(new Error("当前环境不支持微信登录"));
// #endif
});
}
/**
* 获取微信用户手机号
* @param e 微信授权返回的事件对象
* @returns Promise 返回包含加密数据的对象
*/
export function getWxPhoneNumber(
e: any
): Promise<{ code: string; encryptedData?: string; iv?: string }> {
return new Promise((resolve, reject) => {
wxAuthState.value.isLogining = true;
// 判断授权是否成功
if (e.detail.errMsg !== "getPhoneNumber:ok") {
wxAuthState.value.isLogining = false;
wxAuthState.value.authDenied = true;
reject(new Error("用户拒绝授权"));
return;
}
// 获取登录凭证code
getWxLoginCode()
.then((code) => {
// 在微信小程序环境下可以获取encryptedData和iv
// #ifdef MP-WEIXIN
resolve({
code,
encryptedData: e.detail.encryptedData,
iv: e.detail.iv,
});
// #endif
// 其他环境或新版本接口
// #ifndef MP-WEIXIN
resolve({
code,
// 新版本接口在e.detail.code中包含手机号获取凭证
...(e.detail.code ? { phoneCode: e.detail.code } : {}),
});
// #endif
})
.catch((err) => {
reject(err);
})
.finally(() => {
wxAuthState.value.isLogining = false;
});
});
}
/**
* 检查微信会话有效性
* 检查本地token是否存在如存在则检查其有效性
*/
export function checkWxSession(): Promise<boolean> {
return new Promise((resolve) => {
const token = getAccessToken();
if (!token) {
resolve(false);
return;
}
// 调用后端接口验证token有效性
uni.request({
url: "/api/v1/auth/check-session",
method: "GET",
header: {
Authorization: `Bearer ${token}`,
},
success: (res: any) => {
if (res.statusCode === 200 && res.data.valid) {
resolve(true);
} else {
resolve(false);
}
},
fail: () => {
resolve(false);
},
});
});
}
/**
* 获取微信用户信息(如头像、昵称等)
* 注意此接口在2021年4月后的小程序新版本中需要额外授权
*/
export function getWxUserProfile(): Promise<any> {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni.getUserProfile({
desc: "用于完善用户资料",
success: (res) => {
resolve(res.userInfo);
},
fail: (err) => {
reject(err);
},
});
// #endif
// #ifndef MP-WEIXIN
reject(new Error("当前环境不支持获取用户信息"));
// #endif
});
}

View File

@@ -9,7 +9,7 @@ import { Storage } from "@/utils/storage";
export const useUserStore = defineStore("user", () => {
const userInfo = ref<UserInfo | undefined>(getUserInfo());
// 登录
// 账号密码登录
const login = (data: LoginData) => {
return new Promise((resolve, reject) => {
AuthAPI.login(data)
@@ -24,8 +24,8 @@ export const useUserStore = defineStore("user", () => {
});
};
// 微信登录
const loginByWechat = (code: string) => {
// 微信基础授权登录
const loginWithWxCode = (code: string) => {
return new Promise((resolve, reject) => {
AuthAPI.wechatLogin(code)
.then((data) => {
@@ -33,29 +33,14 @@ export const useUserStore = defineStore("user", () => {
resolve(data);
})
.catch((error) => {
console.error("微信登录失败", error);
console.error("微信授权登录失败", error);
reject(error);
});
});
};
// 微信小程序增强登录
const loginByWechatMini = (data: WxLoginData): Promise<any> => {
return new Promise((resolve, reject) => {
AuthAPI.wechatMiniLogin(data)
.then((result) => {
setAccessToken(result.accessToken);
resolve(result);
})
.catch((error) => {
console.error("微信小程序登录失败", error);
reject(error);
});
});
};
// 微信手机号一键登录
const loginByWechatPhone = (data: WxLoginData): Promise<any> => {
// 微信手机号授权登录
const loginWithWxPhone = (data: WxLoginData): Promise<any> => {
return new Promise((resolve, reject) => {
AuthAPI.wechatPhoneLogin(data)
.then((result) => {
@@ -121,9 +106,8 @@ export const useUserStore = defineStore("user", () => {
return {
userInfo,
login,
loginByWechat,
loginByWechatMini,
loginByWechatPhone,
loginWithWxCode,
loginWithWxPhone,
logout,
getInfo,
checkSession,

View File

@@ -140,33 +140,3 @@ export function requireLogin(): void {
});
}
}
/**
* 检查令牌是否过期
* 这是一个简单实现如果需要更精确的检查应该解析JWT的payload
* @returns 是否过期
*/
export function isTokenExpired(token: string): boolean {
if (!token) return true;
try {
// 简单解析JWT payload (不验证签名)
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const payload = JSON.parse(
decodeURIComponent(
atob(base64)
.split("")
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
.join("")
)
);
// 检查过期时间
const now = Math.floor(Date.now() / 1000);
return payload.exp < now;
} catch (e) {
console.error("解析token失败", e);
return true;
}
}

View File

@@ -1,4 +1,4 @@
import { getAccessToken, getRefreshToken, isTokenExpired, setAccessToken } from "./auth";
import { getAccessToken, getRefreshToken, setAccessToken } from "./auth";
// 刷新令牌的锁,防止多个请求同时刷新令牌
let isRefreshing = false;
@@ -58,8 +58,6 @@ interface RequestOptions<T = any> {
header?: Record<string, string>;
timeout?: number;
responseType?: "text" | "arraybuffer";
// 是否跳过令牌刷新 (用于刷新令牌接口本身)
skipTokenRefresh?: boolean;
}
// 请求函数
@@ -89,12 +87,6 @@ function request<T = any>(options: RequestOptions): Promise<T> {
}
// 未授权错误
else if (res.statusCode === 401) {
// 跳过令牌刷新的请求直接返回错误
if (options.skipTokenRefresh) {
reject(new Error("未授权"));
return;
}
// 尝试刷新令牌
if (!isRefreshing) {
isRefreshing = true;
@@ -141,7 +133,7 @@ function request<T = any>(options: RequestOptions): Promise<T> {
};
// 检查令牌是否过期
if (token && !options.skipTokenRefresh && isTokenExpired(token)) {
if (token) {
if (!isRefreshing) {
isRefreshing = true;

2
uni-pages.d.ts vendored
View File

@@ -5,12 +5,12 @@
interface NavigateToOptions {
url: "/pages/index/index" |
"/pages/login/complete-profile" |
"/pages/login/index" |
"/pages/mine/index" |
"/pages/mine/about/index" |
"/pages/mine/faq/index" |
"/pages/mine/feedback/index" |
"/pages/mine/profile/complete-profile" |
"/pages/mine/profile/index" |
"/pages/mine/settings/index" |
"/pages/mine/settings/account/index" |