完成登陆注册接口
This commit is contained in:
70
entry/src/main/ets/api/AuthApi.ets
Normal file
70
entry/src/main/ets/api/AuthApi.ets
Normal file
@@ -0,0 +1,70 @@
|
||||
import { HttpClient } from '../utils/HttpClient';
|
||||
import { TokenStore } from '../utils/TokenStore';
|
||||
import {
|
||||
RegisterRequest,
|
||||
LoginRequest,
|
||||
RefreshTokenRequest,
|
||||
AuthResponseData,
|
||||
TokenPair,
|
||||
AuthMeData,
|
||||
MessageResponse
|
||||
} from '../types/Auth';
|
||||
|
||||
/**
|
||||
* Auth 模块接口
|
||||
* 对应文档: /api/auth/*
|
||||
*/
|
||||
export class AuthApi {
|
||||
/**
|
||||
* POST /api/auth/register
|
||||
* 用户注册
|
||||
*/
|
||||
static async register(req: RegisterRequest): Promise<AuthResponseData> {
|
||||
const data = await HttpClient.post<AuthResponseData>('/api/auth/register', req, false);
|
||||
await TokenStore.saveTokens(data.accessToken, data.refreshToken, data.user.id);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
* 用户登录
|
||||
*/
|
||||
static async login(req: LoginRequest): Promise<AuthResponseData> {
|
||||
const data = await HttpClient.post<AuthResponseData>('/api/auth/login', req, false);
|
||||
await TokenStore.saveTokens(data.accessToken, data.refreshToken, data.user.id);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/refresh
|
||||
* 使用 refreshToken 刷新令牌
|
||||
*/
|
||||
static async refresh(): Promise<TokenPair> {
|
||||
const refreshToken = await TokenStore.getRefreshToken();
|
||||
const req: RefreshTokenRequest = { refreshToken };
|
||||
const data = await HttpClient.post<TokenPair>('/api/auth/refresh', req, false);
|
||||
await TokenStore.saveTokens(data.accessToken, data.refreshToken);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
* 用户登出
|
||||
*/
|
||||
static async logout(): Promise<MessageResponse> {
|
||||
try {
|
||||
const result = await HttpClient.post<MessageResponse>('/api/auth/logout', undefined, true);
|
||||
return result;
|
||||
} finally {
|
||||
await TokenStore.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/auth/me
|
||||
* 获取当前认证用户信息
|
||||
*/
|
||||
static getMe(): Promise<AuthMeData> {
|
||||
return HttpClient.get<AuthMeData>('/api/auth/me', true);
|
||||
}
|
||||
}
|
||||
10
entry/src/main/ets/config/AppConfig.ets
Normal file
10
entry/src/main/ets/config/AppConfig.ets
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 应用配置
|
||||
* 注意:模拟器中无法直接访问主机的 localhost,需要将下面的 BASE_URL 改为:
|
||||
* - 鸿蒙模拟器:使用主机局域网 IP,如 http://192.168.x.x:3000
|
||||
* - 真机调试:手机与电脑在同一局域网下,使用电脑的局域网 IP
|
||||
*/
|
||||
export class AppConfig {
|
||||
static readonly BASE_URL: string = 'http://localhost:3000';
|
||||
static readonly REQUEST_TIMEOUT: number = 10000;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { window } from '@kit.ArkUI';
|
||||
import { TokenStore } from '../utils/TokenStore';
|
||||
|
||||
const DOMAIN = 0x0000;
|
||||
|
||||
@@ -11,6 +12,10 @@ export default class EntryAbility extends UIAbility {
|
||||
} catch (err) {
|
||||
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
|
||||
}
|
||||
// 初始化 Token 存储
|
||||
TokenStore.init(this.context).catch((err: Error) => {
|
||||
hilog.error(DOMAIN, 'testTag', 'TokenStore init failed: %{public}s', err.message);
|
||||
});
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { promptAction, router } from '@kit.ArkUI';
|
||||
import { AuthApi } from '../api/AuthApi';
|
||||
import { HttpError } from '../utils/HttpClient';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct LoginPage {
|
||||
@State username: string = '';
|
||||
@State email: string = '';
|
||||
@State password: string = '';
|
||||
@State isLoading: boolean = false;
|
||||
|
||||
@@ -29,21 +31,22 @@ struct LoginPage {
|
||||
|
||||
// 登录表单
|
||||
Column() {
|
||||
// 用户名输入框
|
||||
// 邮箱输入框
|
||||
Column() {
|
||||
Text('账号')
|
||||
Text('邮箱')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: '请输入手机号/邮箱', text: this.username })
|
||||
TextInput({ placeholder: '请输入邮箱', text: this.email })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.type(InputType.Email)
|
||||
.onChange((value: string) => {
|
||||
this.username = value;
|
||||
this.email = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
@@ -93,8 +96,8 @@ struct LoginPage {
|
||||
.backgroundColor($r('app.color.primary_color'))
|
||||
.borderRadius(24)
|
||||
.margin({ top: 32 })
|
||||
.enabled(!this.isLoading && this.username.length > 0 && this.password.length > 0)
|
||||
.opacity(this.username.length > 0 && this.password.length > 0 ? 1 : 0.6)
|
||||
.enabled(!this.isLoading && this.email.length > 0 && this.password.length > 0)
|
||||
.opacity(this.email.length > 0 && this.password.length > 0 ? 1 : 0.6)
|
||||
.onClick(() => {
|
||||
this.handleLogin();
|
||||
})
|
||||
@@ -125,20 +128,27 @@ struct LoginPage {
|
||||
.backgroundColor($r('app.color.card_background'))
|
||||
}
|
||||
|
||||
private handleLogin(): void {
|
||||
if (this.username.length === 0 || this.password.length === 0) {
|
||||
private async handleLogin(): Promise<void> {
|
||||
if (this.email.length === 0 || this.password.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.username === 'admin' && this.password === 'admin123') {
|
||||
router.replaceUrl({ url: 'pages/HomePage' });
|
||||
} else {
|
||||
this.isLoading = false;
|
||||
promptAction.showToast({ message: '账号或密码错误', duration: 2000 });
|
||||
}
|
||||
}, 500);
|
||||
try {
|
||||
await AuthApi.login({
|
||||
email: this.email,
|
||||
password: this.password
|
||||
});
|
||||
promptAction.showToast({ message: '登录成功', duration: 1500 });
|
||||
router.replaceUrl({ url: 'pages/HomePage' });
|
||||
} catch (err) {
|
||||
const httpErr = err as HttpError;
|
||||
const message = httpErr.code === 401
|
||||
? '邮箱或密码错误'
|
||||
: (httpErr.message || '登录失败,请稍后重试');
|
||||
promptAction.showToast({ message, duration: 2000 });
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { router } from '@kit.ArkUI';
|
||||
import { promptAction, router } from '@kit.ArkUI';
|
||||
import { AuthApi } from '../api/AuthApi';
|
||||
import { HttpError } from '../utils/HttpClient';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct RegisterPage {
|
||||
@State username: string = '';
|
||||
@State email: string = '';
|
||||
@State nickname: string = '';
|
||||
@State password: string = '';
|
||||
@State confirmPassword: string = '';
|
||||
@State isLoading: boolean = false;
|
||||
@@ -37,114 +41,162 @@ struct RegisterPage {
|
||||
.alignItems(VerticalAlign.Center)
|
||||
|
||||
// 表单区域
|
||||
Column() {
|
||||
// 账号
|
||||
Scroll() {
|
||||
Column() {
|
||||
Text('账号')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
// 用户名
|
||||
Column() {
|
||||
Text('用户名')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: '请输入手机号/邮箱', text: this.username })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.onChange((value: string) => {
|
||||
this.username = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// 密码
|
||||
Column() {
|
||||
Text('密码')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: '请输入密码(至少6位)', text: this.password })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.type(InputType.Password)
|
||||
.onChange((value: string) => {
|
||||
this.password = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.margin({ top: 20 })
|
||||
|
||||
// 确认密码
|
||||
Column() {
|
||||
Text('确认密码')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: '请再次输入密码', text: this.confirmPassword })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.type(InputType.Password)
|
||||
.onChange((value: string) => {
|
||||
this.confirmPassword = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.margin({ top: 20 })
|
||||
|
||||
// 密码不一致提示
|
||||
if (this.confirmPassword.length > 0 && this.password !== this.confirmPassword) {
|
||||
Text('两次输入的密码不一致')
|
||||
.fontSize(12)
|
||||
.fontColor('#FF4D4F')
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
|
||||
// 注册按钮
|
||||
Button(this.isLoading ? '注册中...' : '注册')
|
||||
TextInput({ placeholder: '请输入用户名', text: this.username })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.onChange((value: string) => {
|
||||
this.username = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor($r('app.color.primary_color'))
|
||||
.borderRadius(24)
|
||||
.margin({ top: 36 })
|
||||
.enabled(!this.isLoading && this.isFormValid())
|
||||
.opacity(this.isFormValid() ? 1 : 0.6)
|
||||
.onClick(() => {
|
||||
this.handleRegister();
|
||||
})
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
// 返回登录
|
||||
Row() {
|
||||
Text('已有账号?')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
Text('返回登录')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.primary_color'))
|
||||
.fontWeight(FontWeight.Medium)
|
||||
// 邮箱
|
||||
Column() {
|
||||
Text('邮箱')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: '请输入邮箱', text: this.email })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.type(InputType.Email)
|
||||
.onChange((value: string) => {
|
||||
this.email = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.margin({ top: 20 })
|
||||
|
||||
// 昵称(可选)
|
||||
Column() {
|
||||
Text('昵称(可选)')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: '请输入昵称', text: this.nickname })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.onChange((value: string) => {
|
||||
this.nickname = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.margin({ top: 20 })
|
||||
|
||||
// 密码
|
||||
Column() {
|
||||
Text('密码')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: '请输入密码(至少6位)', text: this.password })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.type(InputType.Password)
|
||||
.onChange((value: string) => {
|
||||
this.password = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.margin({ top: 20 })
|
||||
|
||||
// 确认密码
|
||||
Column() {
|
||||
Text('确认密码')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_primary'))
|
||||
.margin({ bottom: 8 })
|
||||
|
||||
TextInput({ placeholder: '请再次输入密码', text: this.confirmPassword })
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.backgroundColor($r('app.color.input_background'))
|
||||
.borderRadius(8)
|
||||
.padding({ left: 16, right: 16 })
|
||||
.type(InputType.Password)
|
||||
.onChange((value: string) => {
|
||||
this.confirmPassword = value;
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
.margin({ top: 20 })
|
||||
|
||||
// 密码不一致提示
|
||||
if (this.confirmPassword.length > 0 && this.password !== this.confirmPassword) {
|
||||
Text('两次输入的密码不一致')
|
||||
.fontSize(12)
|
||||
.fontColor('#FF4D4F')
|
||||
.margin({ top: 8 })
|
||||
}
|
||||
|
||||
// 注册按钮
|
||||
Button(this.isLoading ? '注册中...' : '注册')
|
||||
.width('100%')
|
||||
.height(48)
|
||||
.fontSize(16)
|
||||
.fontColor(Color.White)
|
||||
.backgroundColor($r('app.color.primary_color'))
|
||||
.borderRadius(24)
|
||||
.margin({ top: 36 })
|
||||
.enabled(!this.isLoading && this.isFormValid())
|
||||
.opacity(this.isFormValid() ? 1 : 0.6)
|
||||
.onClick(() => {
|
||||
router.back();
|
||||
this.handleRegister();
|
||||
})
|
||||
|
||||
// 返回登录
|
||||
Row() {
|
||||
Text('已有账号?')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.text_hint'))
|
||||
Text('返回登录')
|
||||
.fontSize(14)
|
||||
.fontColor($r('app.color.primary_color'))
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.onClick(() => {
|
||||
router.back();
|
||||
})
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.margin({ top: 20 })
|
||||
.padding({ bottom: 24 })
|
||||
}
|
||||
.width('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.margin({ top: 20 })
|
||||
.padding({ left: 32, right: 32 })
|
||||
.margin({ top: 24 })
|
||||
}
|
||||
.width('100%')
|
||||
.padding({ left: 32, right: 32 })
|
||||
.margin({ top: 24 })
|
||||
.layoutWeight(1)
|
||||
.scrollBar(BarState.Off)
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
@@ -153,22 +205,34 @@ struct RegisterPage {
|
||||
|
||||
private isFormValid(): boolean {
|
||||
return this.username.length > 0
|
||||
&& this.email.length > 0
|
||||
&& this.password.length >= 6
|
||||
&& this.password === this.confirmPassword;
|
||||
}
|
||||
|
||||
private handleRegister(): void {
|
||||
private async handleRegister(): Promise<void> {
|
||||
if (!this.isFormValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
// 模拟注册请求
|
||||
setTimeout(() => {
|
||||
try {
|
||||
await AuthApi.register({
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
nickname: this.nickname.length > 0 ? this.nickname : undefined
|
||||
});
|
||||
promptAction.showToast({ message: '注册成功', duration: 1500 });
|
||||
router.replaceUrl({ url: 'pages/HomePage' });
|
||||
} catch (err) {
|
||||
const httpErr = err as HttpError;
|
||||
const message = httpErr.code === 409
|
||||
? '用户名或邮箱已被注册'
|
||||
: (httpErr.message || '注册失败,请稍后重试');
|
||||
promptAction.showToast({ message, duration: 2000 });
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
// 注册成功,返回登录页
|
||||
router.back();
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
68
entry/src/main/ets/types/Auth.ets
Normal file
68
entry/src/main/ets/types/Auth.ets
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Auth 相关请求/响应类型定义
|
||||
*/
|
||||
|
||||
// 注册请求
|
||||
export interface RegisterRequest {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
// 登录请求
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// 刷新令牌请求
|
||||
export interface RefreshTokenRequest {
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
// 用户基本信息(登录/注册返回)
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
// 认证响应数据(登录/注册返回的 data 部分)
|
||||
export interface AuthResponseData {
|
||||
user: AuthUser;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
// 令牌对(刷新接口返回的 data 部分)
|
||||
export interface TokenPair {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
// 当前认证用户信息(/auth/me 返回的 data 部分)
|
||||
export interface AuthMeData {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
nickname: string | null;
|
||||
avatar: string | null;
|
||||
bio: string | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
// 通用消息响应
|
||||
export interface MessageResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// 通用包装响应
|
||||
export interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
code?: string;
|
||||
message?: string;
|
||||
}
|
||||
115
entry/src/main/ets/utils/HttpClient.ets
Normal file
115
entry/src/main/ets/utils/HttpClient.ets
Normal file
@@ -0,0 +1,115 @@
|
||||
import { http } from '@kit.NetworkKit';
|
||||
import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { AppConfig } from '../config/AppConfig';
|
||||
import { TokenStore } from './TokenStore';
|
||||
|
||||
/**
|
||||
* HTTP 请求异常
|
||||
*/
|
||||
export class HttpError extends Error {
|
||||
code: number;
|
||||
errorCode: string;
|
||||
|
||||
constructor(message: string, code: number, errorCode: string = '') {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 客户端,统一处理请求/响应、Token 注入、错误转换
|
||||
*/
|
||||
export class HttpClient {
|
||||
/**
|
||||
* 发送请求
|
||||
* @param path 接口路径,如 /api/auth/login
|
||||
* @param method 请求方法
|
||||
* @param body 请求体(对象会自动序列化为 JSON)
|
||||
* @param needAuth 是否需要 Bearer Token(默认 false)
|
||||
* @returns 解析后的 data 字段
|
||||
*/
|
||||
static async request<T>(
|
||||
path: string,
|
||||
method: http.RequestMethod,
|
||||
body?: object,
|
||||
needAuth: boolean = false
|
||||
): Promise<T> {
|
||||
const httpRequest = http.createHttp();
|
||||
try {
|
||||
const url = AppConfig.BASE_URL + path;
|
||||
const header: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (needAuth) {
|
||||
const token = await TokenStore.getAccessToken();
|
||||
if (token.length > 0) {
|
||||
header['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
}
|
||||
|
||||
const options: http.HttpRequestOptions = {
|
||||
method: method,
|
||||
header: header,
|
||||
connectTimeout: AppConfig.REQUEST_TIMEOUT,
|
||||
readTimeout: AppConfig.REQUEST_TIMEOUT,
|
||||
expectDataType: http.HttpDataType.STRING
|
||||
};
|
||||
|
||||
if (body !== undefined && body !== null) {
|
||||
options.extraData = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response: http.HttpResponse = await httpRequest.request(url, options);
|
||||
const status = response.responseCode;
|
||||
const text = response.result as string;
|
||||
|
||||
let parsed: Record<string, ESObject> = {};
|
||||
if (text && text.length > 0) {
|
||||
try {
|
||||
parsed = JSON.parse(text) as Record<string, ESObject>;
|
||||
} catch (e) {
|
||||
throw new HttpError('响应解析失败: ' + text, status);
|
||||
}
|
||||
}
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
// 成功响应统一格式 { data: ... }
|
||||
if (parsed['data'] !== undefined) {
|
||||
return parsed['data'] as T;
|
||||
}
|
||||
return parsed as T;
|
||||
}
|
||||
|
||||
// 错误响应
|
||||
const errorMessage = (parsed['error'] as string) || (parsed['message'] as string) || '请求失败';
|
||||
const errorCode = (parsed['code'] as string) || '';
|
||||
throw new HttpError(errorMessage, status, errorCode);
|
||||
} catch (err) {
|
||||
if (err instanceof HttpError) {
|
||||
throw err as HttpError;
|
||||
}
|
||||
const businessErr = err as BusinessError;
|
||||
throw new HttpError(
|
||||
businessErr.message || '网络请求异常',
|
||||
businessErr.code || -1
|
||||
);
|
||||
} finally {
|
||||
httpRequest.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
static get<T>(path: string, needAuth: boolean = false): Promise<T> {
|
||||
return HttpClient.request<T>(path, http.RequestMethod.GET, undefined, needAuth);
|
||||
}
|
||||
|
||||
static post<T>(path: string, body?: object, needAuth: boolean = false): Promise<T> {
|
||||
return HttpClient.request<T>(path, http.RequestMethod.POST, body, needAuth);
|
||||
}
|
||||
|
||||
static delete<T>(path: string, needAuth: boolean = false): Promise<T> {
|
||||
return HttpClient.request<T>(path, http.RequestMethod.DELETE, undefined, needAuth);
|
||||
}
|
||||
}
|
||||
74
entry/src/main/ets/utils/TokenStore.ets
Normal file
74
entry/src/main/ets/utils/TokenStore.ets
Normal file
@@ -0,0 +1,74 @@
|
||||
import { preferences } from '@kit.ArkData';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
|
||||
/**
|
||||
* Token 持久化存储工具,基于 @ohos.data.preferences 实现
|
||||
*/
|
||||
export class TokenStore {
|
||||
private static readonly PREF_NAME: string = 'auth_pref';
|
||||
private static readonly KEY_ACCESS_TOKEN: string = 'access_token';
|
||||
private static readonly KEY_REFRESH_TOKEN: string = 'refresh_token';
|
||||
private static readonly KEY_USER_ID: string = 'user_id';
|
||||
|
||||
private static prefs: preferences.Preferences | null = null;
|
||||
|
||||
// 内存中的 Token 缓存,用于同步获取
|
||||
private static accessTokenCache: string = '';
|
||||
|
||||
static async init(context: common.UIAbilityContext | Context): Promise<void> {
|
||||
if (TokenStore.prefs) {
|
||||
return;
|
||||
}
|
||||
TokenStore.prefs = await preferences.getPreferences(context, TokenStore.PREF_NAME);
|
||||
TokenStore.accessTokenCache = await TokenStore.prefs.get(TokenStore.KEY_ACCESS_TOKEN, '') as string;
|
||||
}
|
||||
|
||||
static async saveTokens(accessToken: string, refreshToken: string, userId?: string): Promise<void> {
|
||||
if (!TokenStore.prefs) {
|
||||
throw new Error('TokenStore 未初始化,请先调用 TokenStore.init(context)');
|
||||
}
|
||||
await TokenStore.prefs.put(TokenStore.KEY_ACCESS_TOKEN, accessToken);
|
||||
await TokenStore.prefs.put(TokenStore.KEY_REFRESH_TOKEN, refreshToken);
|
||||
if (userId) {
|
||||
await TokenStore.prefs.put(TokenStore.KEY_USER_ID, userId);
|
||||
}
|
||||
await TokenStore.prefs.flush();
|
||||
TokenStore.accessTokenCache = accessToken;
|
||||
}
|
||||
|
||||
static async getAccessToken(): Promise<string> {
|
||||
if (!TokenStore.prefs) {
|
||||
return '';
|
||||
}
|
||||
const value = await TokenStore.prefs.get(TokenStore.KEY_ACCESS_TOKEN, '') as string;
|
||||
TokenStore.accessTokenCache = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
static async getRefreshToken(): Promise<string> {
|
||||
if (!TokenStore.prefs) {
|
||||
return '';
|
||||
}
|
||||
return await TokenStore.prefs.get(TokenStore.KEY_REFRESH_TOKEN, '') as string;
|
||||
}
|
||||
|
||||
static getAccessTokenSync(): string {
|
||||
return TokenStore.accessTokenCache;
|
||||
}
|
||||
|
||||
static async clear(): Promise<void> {
|
||||
if (!TokenStore.prefs) {
|
||||
return;
|
||||
}
|
||||
await TokenStore.prefs.delete(TokenStore.KEY_ACCESS_TOKEN);
|
||||
await TokenStore.prefs.delete(TokenStore.KEY_REFRESH_TOKEN);
|
||||
await TokenStore.prefs.delete(TokenStore.KEY_USER_ID);
|
||||
await TokenStore.prefs.flush();
|
||||
TokenStore.accessTokenCache = '';
|
||||
}
|
||||
|
||||
static async isLoggedIn(): Promise<boolean> {
|
||||
const token = await TokenStore.getAccessToken();
|
||||
return token.length > 0;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,14 @@
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": false,
|
||||
"pages": "$profile:main_pages",
|
||||
"requestPermissions": [
|
||||
{
|
||||
"name": "ohos.permission.INTERNET"
|
||||
},
|
||||
{
|
||||
"name": "ohos.permission.GET_NETWORK_INFO"
|
||||
}
|
||||
],
|
||||
"abilities": [
|
||||
{
|
||||
"name": "EntryAbility",
|
||||
|
||||
Reference in New Issue
Block a user