feat(auth): 正式登录系统——华为账号登录(Account Kit,client_id 占位拦截防 crash)+ 邮箱验证码登录(发码冷却倒计时);dev-token 与服务器地址挪入开发者高级设置;client_id 填入 6917613748100335131
This commit is contained in:
@@ -4,8 +4,10 @@
|
||||
* - access 短令牌放内存 + preferences(local 明文可接受:V1 骨架,无支付;
|
||||
* 正式版应迁 @ohos.security.asset,注释留迁移点)
|
||||
* - refresh 一次性轮换:每次 refresh 换新对
|
||||
* - dev-token 登录:POST /auth/dev-token(后端 DEV_AUTH_ENABLED 本地可用;
|
||||
* 华为账号/短信登录是后续通道,接口按多通道预留)
|
||||
* - dev-token 登录:POST /auth/dev-token(后端 DEV_AUTH_ENABLED 本地可用,
|
||||
* 挪进登录页「高级设置(开发者)」折叠区,仅供本机联调)
|
||||
* - 华为账号登录:POST /auth/huawei(Account Kit idToken 换双令牌)
|
||||
* - 邮箱验证码登录:POST /auth/email/code + /auth/email/login
|
||||
* - ArkTS 约束:所有请求/响应都是具名 interface;字面量先声明类型再传
|
||||
*/
|
||||
|
||||
@@ -29,6 +31,11 @@ interface AuthWireResp {
|
||||
user_id: string
|
||||
}
|
||||
|
||||
interface EmailCodeWireResp {
|
||||
sent: boolean
|
||||
cooldown_seconds: number
|
||||
}
|
||||
|
||||
const PREF_NAME = 'meida_auth'
|
||||
const KEY_TOKENS = 'tokens'
|
||||
const KEY_BASE_URL = 'base_url'
|
||||
@@ -74,12 +81,38 @@ export class AuthStore {
|
||||
return await AuthStore.prefs!.get(KEY_BASE_URL, DEFAULT_BASE_URL) as string
|
||||
}
|
||||
|
||||
/** dev-token 登录(本地后端开发通道)。失败由 ApiClient 抛 ApiError。 */
|
||||
/** dev-token 登录(本机联调通道,入口在登录页高级设置折叠区)。 */
|
||||
static async loginByDevToken(deviceKey: string): Promise<void> {
|
||||
const req: Record<string, Object> = { 'device_key': deviceKey }
|
||||
// 登录本身不能走带鉴权的 post()——ApiClient 对未登录一律 40100 短路,
|
||||
// 登录接口会被自己的守卫拦死(鸡生蛋)。必须走无鉴权通道。
|
||||
const data = await ApiClient.postWithoutAuth<AuthWireResp>('/api/v1/auth/dev-token', req)
|
||||
await AuthStore.loginAndSave('/api/v1/auth/dev-token', req)
|
||||
}
|
||||
|
||||
/** 华为账号登录:Account Kit idToken 换双令牌。失败由 ApiClient 抛 ApiError。 */
|
||||
static async loginByHuaweiIdToken(idToken: string): Promise<void> {
|
||||
const req: Record<string, Object> = { 'id_token': idToken }
|
||||
await AuthStore.loginAndSave('/api/v1/auth/huawei', req)
|
||||
}
|
||||
|
||||
/** 发送邮箱验证码。成功返回服务端给的 cooldown_seconds(前端冷却倒计时用)。 */
|
||||
static async sendEmailCode(email: string): Promise<number> {
|
||||
const req: Record<string, Object> = { 'email': email }
|
||||
const data = await ApiClient.postWithoutAuth<EmailCodeWireResp>('/api/v1/auth/email/code', req)
|
||||
return data.cooldown_seconds
|
||||
}
|
||||
|
||||
/** 邮箱验证码登录:同双令牌持久化。 */
|
||||
static async loginByEmail(email: string, code: string): Promise<void> {
|
||||
const req: Record<string, Object> = { 'email': email, 'code': code }
|
||||
await AuthStore.loginAndSave('/api/v1/auth/email/login', req)
|
||||
}
|
||||
|
||||
/**
|
||||
* 各登录通道公共收尾:无鉴权通道换双令牌并持久化。
|
||||
* 登录本身不能走带鉴权的 post()——ApiClient 对未登录一律 40100 短路,
|
||||
* 登录接口会被自己的守卫拦死(鸡生蛋)。必须走无鉴权通道。
|
||||
*/
|
||||
private static async loginAndSave(path: string, req: Record<string, Object>): Promise<void> {
|
||||
const data = await ApiClient.postWithoutAuth<AuthWireResp>(path, req)
|
||||
await AuthStore.saveTokens(AuthStore.fromWire(data))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 华为账号授权登录封装(Account Kit)。
|
||||
*
|
||||
* 本机 SDK 声明为准(sdk/default/hms/ets/api/@hms.core.authentication.d.ts):
|
||||
* - `new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest()`
|
||||
* - `new authentication.AuthenticationController(context).executeRequest(request)`
|
||||
* → Promise<AuthenticationResponse>,成功时 as AuthorizationWithHuaweiIDResponse
|
||||
* 取 `data.idToken`(即后端 POST /auth/huawei 的 id_token)
|
||||
* - 用户取消授权:executeRequest reject,BusinessError.code = 1001502012
|
||||
* (AuthenticationErrorCode.USER_CANCELED),UI 层静默处理
|
||||
*
|
||||
* client_id 来自 module.json5 模块级 metadata(AGC 平台申请后填入);
|
||||
* 仍是占位值时 LoginPage 直接拦截给提示,不会真正调 SDK。
|
||||
*/
|
||||
|
||||
import { authentication } from '@kit.AccountKit'
|
||||
import { bundleManager } from '@kit.AbilityKit'
|
||||
import { BusinessError } from '@kit.BasicServicesKit'
|
||||
import { ContextHolder } from './ContextHolder'
|
||||
|
||||
/** module.json5 metadata 里的 client_id 占位值(未接 AGC 前保持此值)。 */
|
||||
export const HUAWEI_CLIENT_ID_PLACEHOLDER: string = 'TODO_AGC_CLIENT_ID'
|
||||
|
||||
/** Account Kit 错误码:用户取消授权。 */
|
||||
const ERR_USER_CANCELED: number = 1001502012
|
||||
|
||||
export class HuaweiLogin {
|
||||
/** 读取本模块 module.json5 里配置的 client_id;读取失败或未配置返回占位值。 */
|
||||
static async resolveClientId(): Promise<string> {
|
||||
try {
|
||||
const flag = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE |
|
||||
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_METADATA
|
||||
const info = await bundleManager.getBundleInfoForSelf(flag)
|
||||
for (const hap of info.hapModulesInfo) {
|
||||
for (const item of hap.metadata) {
|
||||
if (item.name === 'client_id') {
|
||||
return item.value
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
// 读取失败按未配置处理,页面给「暂未开放」提示
|
||||
}
|
||||
return HUAWEI_CLIENT_ID_PLACEHOLDER
|
||||
}
|
||||
|
||||
/** 是否已完成 AGC 配置(真实 client_id 才能拉起授权页)。 */
|
||||
static isConfigured(clientId: string): boolean {
|
||||
return clientId !== '' && clientId !== HUAWEI_CLIENT_ID_PLACEHOLDER
|
||||
}
|
||||
|
||||
/** 是否用户主动取消授权(取消不算错误,UI 静默返回)。 */
|
||||
static isUserCancelled(e: Object): boolean {
|
||||
if (!(e instanceof Error)) {
|
||||
return false
|
||||
}
|
||||
const be = e as BusinessError
|
||||
return be.code === ERR_USER_CANCELED
|
||||
}
|
||||
|
||||
/** 拉起华为账号授权页(非静默,scopes: openid + profile),返回 idToken。 */
|
||||
static async requestIdToken(): Promise<string> {
|
||||
const controller = new authentication.AuthenticationController(ContextHolder.get())
|
||||
const provider = new authentication.HuaweiIDProvider()
|
||||
const request = provider.createAuthorizationWithHuaweiIDRequest()
|
||||
request.scopes = ['openid', 'profile']
|
||||
request.forceAuthorization = true
|
||||
const response = await controller.executeRequest(request)
|
||||
const authResp = response as authentication.AuthorizationWithHuaweiIDResponse
|
||||
const idToken = authResp.data?.idToken
|
||||
if (idToken === undefined || idToken === '') {
|
||||
throw new Error('华为授权成功但未返回登录凭证')
|
||||
}
|
||||
return idToken
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* LoginPage(F1)—— dev-token 开发登录页。
|
||||
* LoginPage(F1)—— 正式登录页:华为账号登录 + 邮箱验证码登录。
|
||||
*
|
||||
* 闭环:设备标识(随机生成一次并持久化,可改)→ AuthStore.loginByDevToken
|
||||
* → 成功 router.replaceUrl HomePage;失败 catch ApiError 展示 message+hint。
|
||||
* 高级设置:折叠区编辑 baseUrl(AuthStore.setBaseUrl),真机连开发机靠它。
|
||||
* 主区(白卡):
|
||||
* - 华为账号一键登录:Account Kit 拉起授权页拿 idToken → /auth/huawei
|
||||
* (client_id 未配置时给「暂未开放」提示,不调 SDK)
|
||||
* - 邮箱验证码登录:sendEmailCode(60s 冷却倒计时)→ /auth/email/login
|
||||
* 高级设置(开发者,默认收起):dev-token 内测登录 + 服务器地址编辑,本机联调用。
|
||||
* 已登录态进入本页:aboutToAppear 直接跳 HomePage。
|
||||
*
|
||||
* ArkTS 约束(对齐 core/AuthStore 风格):
|
||||
@@ -13,14 +15,22 @@
|
||||
|
||||
import router from '@ohos.router'
|
||||
import preferences from '@ohos.data.preferences'
|
||||
import { promptAction } from '@kit.ArkUI'
|
||||
import { ApiError } from '../core/ApiTypes'
|
||||
import { AuthStore, DEFAULT_BASE_URL } from '../core/AuthStore'
|
||||
import { ContextHolder } from '../core/ContextHolder'
|
||||
import { HuaweiLogin } from '../core/HuaweiLogin'
|
||||
import { COLOR_PRIMARY } from '../core/Theme'
|
||||
import { ErrorBanner } from '../components/f1/ErrorBanner'
|
||||
|
||||
const PREF_NAME_LOGIN = 'meida_login'
|
||||
const KEY_DEVICE_KEY = 'device_key'
|
||||
const EMAIL_CODE_LENGTH: number = 6
|
||||
|
||||
/** 宽松邮箱校验:非空局部 @ 非空局部 . 非空局部。 */
|
||||
function isEmail(text: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text)
|
||||
}
|
||||
|
||||
/** 随机设备标识:dev- + 16 位 hex,生成一次后随 preferences 持久化。 */
|
||||
function generateDeviceKey(): string {
|
||||
@@ -35,20 +45,32 @@ function generateDeviceKey(): string {
|
||||
@Entry
|
||||
@Component
|
||||
struct LoginPage {
|
||||
@State huaweiClientId: string = ''
|
||||
@State huaweiLogging: boolean = false
|
||||
@State emailInput: string = ''
|
||||
@State emailCode: string = ''
|
||||
@State sendingCode: boolean = false
|
||||
@State sendCooldown: number = 0
|
||||
@State emailLogging: boolean = false
|
||||
@State deviceKey: string = ''
|
||||
@State devLogging: boolean = false
|
||||
@State baseUrlInput: string = ''
|
||||
@State showAdvanced: boolean = false
|
||||
@State logging: boolean = false
|
||||
@State savingUrl: boolean = false
|
||||
@State errMessage: string = ''
|
||||
@State errHint: string = ''
|
||||
@State savedTip: string = ''
|
||||
private cooldownTimer: number = -1
|
||||
|
||||
aboutToAppear(): void {
|
||||
this.bootstrap()
|
||||
}
|
||||
|
||||
/** 具名 async 初始化:确保令牌已载入 → 已登录直跳 → 回填设备标识与地址。 */
|
||||
aboutToDisappear(): void {
|
||||
this.stopCooldownTick()
|
||||
}
|
||||
|
||||
/** 具名 async 初始化:令牌载入 → 已登录直跳 → 回填表单与 client_id。 */
|
||||
private async bootstrap(): Promise<void> {
|
||||
try {
|
||||
await AuthStore.init()
|
||||
@@ -73,11 +95,112 @@ struct LoginPage {
|
||||
this.deviceKey = generateDeviceKey()
|
||||
}
|
||||
this.baseUrlInput = await AuthStore.getBaseUrl()
|
||||
this.huaweiClientId = await HuaweiLogin.resolveClientId()
|
||||
}
|
||||
|
||||
/** 登录主流程:校验 → loading → dev-token → 跳首页;失败横幅展示。 */
|
||||
private async doLogin(): Promise<void> {
|
||||
if (this.logging) {
|
||||
/** 华为账号登录:占位 client_id 直接提示;取消静默;其余 ApiError 走横幅。 */
|
||||
private async doHuaweiLogin(): Promise<void> {
|
||||
if (this.huaweiLogging) {
|
||||
return
|
||||
}
|
||||
this.errMessage = ''
|
||||
this.errHint = ''
|
||||
if (!HuaweiLogin.isConfigured(this.huaweiClientId)) {
|
||||
this.errMessage = '华为登录暂未开放'
|
||||
this.errHint = '请先用邮箱验证码登录,正式开放后第一时间支持'
|
||||
return
|
||||
}
|
||||
this.huaweiLogging = true
|
||||
try {
|
||||
const idToken = await HuaweiLogin.requestIdToken()
|
||||
await AuthStore.loginByHuaweiIdToken(idToken)
|
||||
router.replaceUrl({ url: 'pages/HomePage' })
|
||||
} catch (e) {
|
||||
if (HuaweiLogin.isUserCancelled(e)) {
|
||||
// 用户取消授权:不报错
|
||||
} else if (e instanceof ApiError) {
|
||||
this.errMessage = e.message
|
||||
this.errHint = e.hint !== undefined ? e.hint : ''
|
||||
} else {
|
||||
this.errMessage = '华为授权失败,请稍后重试'
|
||||
this.errHint = '也可使用邮箱验证码登录'
|
||||
}
|
||||
} finally {
|
||||
this.huaweiLogging = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送邮箱验证码:校验格式 → 请求 → 按服务端 cooldown 启动倒计时。 */
|
||||
private async doSendCode(): Promise<void> {
|
||||
if (this.sendingCode || this.sendCooldown > 0) {
|
||||
return
|
||||
}
|
||||
const email = this.emailInput.trim()
|
||||
this.errMessage = ''
|
||||
this.errHint = ''
|
||||
if (!isEmail(email)) {
|
||||
this.errMessage = '邮箱格式不正确'
|
||||
this.errHint = '示例:name@example.com'
|
||||
return
|
||||
}
|
||||
this.sendingCode = true
|
||||
try {
|
||||
const cooldown = await AuthStore.sendEmailCode(email)
|
||||
this.sendCooldown = cooldown > 0 ? cooldown : 60
|
||||
this.startCooldownTick()
|
||||
promptAction.showToast({ message: '验证码已发送,请查收邮件', duration: 2000 })
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
this.errMessage = e.message
|
||||
this.errHint = e.hint !== undefined ? e.hint : ''
|
||||
} else {
|
||||
this.errMessage = '验证码发送失败,请检查网络后重试'
|
||||
this.errHint = ''
|
||||
}
|
||||
} finally {
|
||||
this.sendingCode = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 邮箱验证码登录:校验 → 请求 → 跳首页;失败横幅展示。 */
|
||||
private async doEmailLogin(): Promise<void> {
|
||||
if (this.emailLogging) {
|
||||
return
|
||||
}
|
||||
const email = this.emailInput.trim()
|
||||
const code = this.emailCode.trim()
|
||||
this.errMessage = ''
|
||||
this.errHint = ''
|
||||
if (!isEmail(email)) {
|
||||
this.errMessage = '邮箱格式不正确'
|
||||
this.errHint = '示例:name@example.com'
|
||||
return
|
||||
}
|
||||
if (code.length !== EMAIL_CODE_LENGTH) {
|
||||
this.errMessage = '请输入 6 位数字验证码'
|
||||
this.errHint = ''
|
||||
return
|
||||
}
|
||||
this.emailLogging = true
|
||||
try {
|
||||
await AuthStore.loginByEmail(email, code)
|
||||
router.replaceUrl({ url: 'pages/HomePage' })
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
this.errMessage = e.message
|
||||
this.errHint = e.hint !== undefined ? e.hint : ''
|
||||
} else {
|
||||
this.errMessage = '登录失败,请检查网络后重试'
|
||||
this.errHint = ''
|
||||
}
|
||||
} finally {
|
||||
this.emailLogging = false
|
||||
}
|
||||
}
|
||||
|
||||
/** dev-token 内测登录(高级设置里,本机联调用)。 */
|
||||
private async doDevLogin(): Promise<void> {
|
||||
if (this.devLogging) {
|
||||
return
|
||||
}
|
||||
const key = this.deviceKey.trim()
|
||||
@@ -89,7 +212,7 @@ struct LoginPage {
|
||||
this.errHint = '可点「随机生成」重新生成一个'
|
||||
return
|
||||
}
|
||||
this.logging = true
|
||||
this.devLogging = true
|
||||
this.persistDeviceKey(key)
|
||||
try {
|
||||
await AuthStore.loginByDevToken(key)
|
||||
@@ -99,12 +222,11 @@ struct LoginPage {
|
||||
this.errMessage = e.message
|
||||
this.errHint = e.hint !== undefined ? e.hint : ''
|
||||
} else {
|
||||
// 连接层失败(未进统一信封,#25):给内测口径指引,不改 ApiClient 全局文案
|
||||
this.errMessage = '无法连接内测服务,请确认已连接指定网络,或联系测试人员'
|
||||
this.errHint = ''
|
||||
}
|
||||
} finally {
|
||||
this.logging = false
|
||||
this.devLogging = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +241,24 @@ struct LoginPage {
|
||||
}
|
||||
}
|
||||
|
||||
private startCooldownTick(): void {
|
||||
this.stopCooldownTick()
|
||||
this.cooldownTimer = setInterval(() => {
|
||||
if (this.sendCooldown > 0) {
|
||||
this.sendCooldown -= 1
|
||||
} else {
|
||||
this.stopCooldownTick()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private stopCooldownTick(): void {
|
||||
if (this.cooldownTimer !== -1) {
|
||||
clearInterval(this.cooldownTimer)
|
||||
this.cooldownTimer = -1
|
||||
}
|
||||
}
|
||||
|
||||
private async saveBaseUrl(): Promise<void> {
|
||||
const url = this.baseUrlInput.trim()
|
||||
this.savedTip = ''
|
||||
@@ -170,7 +310,7 @@ struct LoginPage {
|
||||
Column() {
|
||||
Scroll() {
|
||||
Column({ space: 22 }) {
|
||||
// 品牌区
|
||||
// 品牌区(正式产品口径)
|
||||
Column({ space: 10 }) {
|
||||
Text('美搭')
|
||||
.fontSize(48)
|
||||
@@ -180,54 +320,94 @@ struct LoginPage {
|
||||
Text('AI 穿搭美学助手 · 每天三分钟,搭出好气质')
|
||||
.fontSize(14)
|
||||
.fontColor('#8A8A8A')
|
||||
// 内测标识(#25):常驻,不藏进高级设置
|
||||
Text('内测版 · 需连接指定网络后使用')
|
||||
.fontSize(12)
|
||||
.fontColor('#B3554D')
|
||||
.padding({ left: 14, right: 14, top: 5, bottom: 5 })
|
||||
.borderRadius(14)
|
||||
.backgroundColor('#F7E3EA')
|
||||
}
|
||||
.margin({ top: 72, bottom: 8 })
|
||||
|
||||
// 内测账号标识(#25:话术去开发黑话,说明账号机制)
|
||||
Column({ space: 8 }) {
|
||||
Row({ space: 8 }) {
|
||||
Text('内测账号标识')
|
||||
.fontSize(14)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#2B2B2B')
|
||||
Blank()
|
||||
Text('随机生成')
|
||||
.fontSize(13)
|
||||
.fontColor(COLOR_PRIMARY)
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.borderRadius(12)
|
||||
.backgroundColor('#F7E3EA')
|
||||
.onClick(() => this.regenDeviceKey())
|
||||
// 主登录卡:华为账号 + 邮箱验证码
|
||||
Column({ space: 14 }) {
|
||||
Button(this.huaweiLogging ? '正在拉起华为授权…' : '华为账号一键登录')
|
||||
.width('100%')
|
||||
.height(50)
|
||||
.fontSize(17)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.borderRadius(25)
|
||||
.backgroundColor(COLOR_PRIMARY)
|
||||
.fontColor('#FFFFFF')
|
||||
.enabled(!this.huaweiLogging)
|
||||
.onClick(() => this.doHuaweiLogin())
|
||||
|
||||
// 分隔线:或使用邮箱登录
|
||||
Row({ space: 12 }) {
|
||||
Divider()
|
||||
.layoutWeight(1)
|
||||
.strokeWidth(1)
|
||||
.color('#E5E7EB')
|
||||
Text('或使用邮箱登录')
|
||||
.fontSize(12)
|
||||
.fontColor('#B3B3B3')
|
||||
Divider()
|
||||
.layoutWeight(1)
|
||||
.strokeWidth(1)
|
||||
.color('#E5E7EB')
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
TextInput({ text: this.deviceKey, placeholder: 'dev-xxxxxxxxxxxxxxxx' })
|
||||
TextInput({ text: this.emailInput, placeholder: '邮箱地址' })
|
||||
.height(48)
|
||||
.fontSize(15)
|
||||
.type(InputType.Email)
|
||||
.borderRadius(12)
|
||||
.backgroundColor('#FFFFFF')
|
||||
.backgroundColor('#F7F5F6')
|
||||
.padding({ left: 14, right: 14 })
|
||||
.onChange((value: string) => {
|
||||
this.deviceKey = value
|
||||
this.emailInput = value
|
||||
})
|
||||
|
||||
Text('无需注册手机号;同一标识对应同一账号,换一个标识相当于新账号')
|
||||
.fontSize(11)
|
||||
.fontColor('#B3B3B3')
|
||||
Row({ space: 10 }) {
|
||||
TextInput({ text: this.emailCode, placeholder: '6 位验证码' })
|
||||
.height(48)
|
||||
.fontSize(15)
|
||||
.type(InputType.Number)
|
||||
.maxLength(EMAIL_CODE_LENGTH)
|
||||
.borderRadius(12)
|
||||
.backgroundColor('#F7F5F6')
|
||||
.padding({ left: 14, right: 14 })
|
||||
.layoutWeight(1)
|
||||
.onChange((value: string) => {
|
||||
this.emailCode = value.replace(/[^0-9]/g, '').slice(0, EMAIL_CODE_LENGTH)
|
||||
})
|
||||
|
||||
Button(this.sendCooldown > 0 ? `${this.sendCooldown}s 后重发` : (this.sendingCode ? '发送中…' : '发送验证码'))
|
||||
.fontSize(14)
|
||||
.height(48)
|
||||
.borderRadius(12)
|
||||
.backgroundColor(this.sendCooldown > 0 ? '#F0E0E6' : COLOR_PRIMARY)
|
||||
.fontColor(this.sendCooldown > 0 ? '#B3B3B3' : '#FFFFFF')
|
||||
.enabled(!this.sendingCode && this.sendCooldown <= 0)
|
||||
.onClick(() => this.doSendCode())
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Button(this.emailLogging ? '正在登录…' : '登录')
|
||||
.width('100%')
|
||||
.height(50)
|
||||
.fontSize(17)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.borderRadius(25)
|
||||
.backgroundColor(COLOR_PRIMARY)
|
||||
.fontColor('#FFFFFF')
|
||||
.enabled(!this.emailLogging && isEmail(this.emailInput.trim()) && this.emailCode.trim().length === EMAIL_CODE_LENGTH)
|
||||
.onClick(() => this.doEmailLogin())
|
||||
}
|
||||
.width('100%')
|
||||
.padding(14)
|
||||
.borderRadius(16)
|
||||
.backgroundColor('#FFFFFF')
|
||||
|
||||
// 高级设置(服务器地址,真机连开发机靠它)
|
||||
// 高级设置(开发者):dev-token 内测登录 + 服务器地址,默认收起
|
||||
Column({ space: 10 }) {
|
||||
Row() {
|
||||
Text('高级设置 · 服务器地址')
|
||||
Text('高级设置(开发者)')
|
||||
.fontSize(14)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#2B2B2B')
|
||||
@@ -244,11 +424,54 @@ struct LoginPage {
|
||||
|
||||
if (this.showAdvanced) {
|
||||
Column({ space: 8 }) {
|
||||
TextInput({ text: this.baseUrlInput, placeholder: DEFAULT_BASE_URL })
|
||||
.height(46)
|
||||
// dev-token 内测登录(本机联调通道)
|
||||
Row({ space: 8 }) {
|
||||
Text('内测账号标识')
|
||||
.fontSize(13)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.fontColor('#2B2B2B')
|
||||
Blank()
|
||||
Text('随机生成')
|
||||
.fontSize(12)
|
||||
.fontColor(COLOR_PRIMARY)
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.borderRadius(12)
|
||||
.backgroundColor('#F7E3EA')
|
||||
.onClick(() => this.regenDeviceKey())
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
TextInput({ text: this.deviceKey, placeholder: 'dev-xxxxxxxxxxxxxxxx' })
|
||||
.height(44)
|
||||
.fontSize(14)
|
||||
.borderRadius(12)
|
||||
.backgroundColor('#FFFFFF')
|
||||
.backgroundColor('#F7F5F6')
|
||||
.padding({ left: 14, right: 14 })
|
||||
.onChange((value: string) => {
|
||||
this.deviceKey = value
|
||||
})
|
||||
|
||||
Button(this.devLogging ? '正在进入…' : '内测通道进入')
|
||||
.width('100%')
|
||||
.height(42)
|
||||
.fontSize(15)
|
||||
.borderRadius(21)
|
||||
.backgroundColor('#F0E0E6')
|
||||
.fontColor(COLOR_PRIMARY)
|
||||
.enabled(!this.devLogging && this.deviceKey.trim() !== '')
|
||||
.onClick(() => this.doDevLogin())
|
||||
|
||||
Divider()
|
||||
.width('100%')
|
||||
.strokeWidth(1)
|
||||
.color('#E5E7EB')
|
||||
.margin({ top: 4, bottom: 4 })
|
||||
|
||||
TextInput({ text: this.baseUrlInput, placeholder: DEFAULT_BASE_URL })
|
||||
.height(44)
|
||||
.fontSize(14)
|
||||
.borderRadius(12)
|
||||
.backgroundColor('#F7F5F6')
|
||||
.padding({ left: 14, right: 14 })
|
||||
.onChange((value: string) => {
|
||||
this.baseUrlInput = value
|
||||
@@ -281,7 +504,7 @@ struct LoginPage {
|
||||
}
|
||||
.width('100%')
|
||||
|
||||
Text('内测版需连接指定网络;如无法登录,请联系测试人员')
|
||||
Text('内测通道仅供开发联调;如无法登录,请联系测试人员')
|
||||
.fontSize(11)
|
||||
.fontColor('#B3B3B3')
|
||||
}
|
||||
@@ -294,19 +517,7 @@ struct LoginPage {
|
||||
.borderRadius(16)
|
||||
.backgroundColor('#FFFFFF')
|
||||
|
||||
// 登录按钮
|
||||
Button(this.logging ? '正在进入…' : '进入美搭')
|
||||
.width('100%')
|
||||
.height(50)
|
||||
.fontSize(17)
|
||||
.fontWeight(FontWeight.Medium)
|
||||
.borderRadius(25)
|
||||
.backgroundColor(COLOR_PRIMARY)
|
||||
.fontColor('#FFFFFF')
|
||||
.enabled(!this.logging && this.deviceKey.trim() !== '')
|
||||
.onClick(() => this.doLogin())
|
||||
|
||||
// 错误横幅(登录/地址保存失败)
|
||||
// 错误横幅(登录/发送验证码/地址保存失败)
|
||||
ErrorBanner({
|
||||
message: this.errMessage,
|
||||
hint: this.errHint,
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
],
|
||||
"installationFree": false,
|
||||
"pages": "$profile:main_pages",
|
||||
"metadata": [
|
||||
{
|
||||
"name": "client_id",
|
||||
"value": "6917613748100335131"
|
||||
}
|
||||
],
|
||||
"abilities": [
|
||||
{
|
||||
"name": "EntryAbility",
|
||||
|
||||
Reference in New Issue
Block a user