From 2b0df49aa7aafed100ac1fb518636c78adcc1444 Mon Sep 17 00:00:00 2001 From: xiangsilian Date: Tue, 15 Sep 2026 00:03:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=AD=A3=E5=BC=8F=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E7=B3=BB=E7=BB=9F=E2=80=94=E2=80=94=E5=8D=8E=E4=B8=BA?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E7=99=BB=E5=BD=95=EF=BC=88Account=20Kit?= =?UTF-8?q?=EF=BC=8Cclient=5Fid=20=E5=8D=A0=E4=BD=8D=E6=8B=A6=E6=88=AA?= =?UTF-8?q?=E9=98=B2=20crash=EF=BC=89+=20=E9=82=AE=E7=AE=B1=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E7=99=BB=E5=BD=95=EF=BC=88=E5=8F=91=E7=A0=81?= =?UTF-8?q?=E5=86=B7=E5=8D=B4=E5=80=92=E8=AE=A1=E6=97=B6=EF=BC=89=EF=BC=9B?= =?UTF-8?q?dev-token=20=E4=B8=8E=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E6=8C=AA=E5=85=A5=E5=BC=80=E5=8F=91=E8=80=85=E9=AB=98?= =?UTF-8?q?=E7=BA=A7=E8=AE=BE=E7=BD=AE=EF=BC=9Bclient=5Fid=20=E5=A1=AB?= =?UTF-8?q?=E5=85=A5=206917613748100335131?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entry/src/main/ets/core/AuthStore.ets | 45 ++- app/entry/src/main/ets/core/HuaweiLogin.ets | 76 +++++ app/entry/src/main/ets/pages/LoginPage.ets | 331 ++++++++++++++++---- app/entry/src/main/module.json5 | 6 + 4 files changed, 392 insertions(+), 66 deletions(-) create mode 100644 app/entry/src/main/ets/core/HuaweiLogin.ets diff --git a/app/entry/src/main/ets/core/AuthStore.ets b/app/entry/src/main/ets/core/AuthStore.ets index 930cb93..b582c4f 100644 --- a/app/entry/src/main/ets/core/AuthStore.ets +++ b/app/entry/src/main/ets/core/AuthStore.ets @@ -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 { const req: Record = { 'device_key': deviceKey } - // 登录本身不能走带鉴权的 post()——ApiClient 对未登录一律 40100 短路, - // 登录接口会被自己的守卫拦死(鸡生蛋)。必须走无鉴权通道。 - const data = await ApiClient.postWithoutAuth('/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 { + const req: Record = { 'id_token': idToken } + await AuthStore.loginAndSave('/api/v1/auth/huawei', req) + } + + /** 发送邮箱验证码。成功返回服务端给的 cooldown_seconds(前端冷却倒计时用)。 */ + static async sendEmailCode(email: string): Promise { + const req: Record = { 'email': email } + const data = await ApiClient.postWithoutAuth('/api/v1/auth/email/code', req) + return data.cooldown_seconds + } + + /** 邮箱验证码登录:同双令牌持久化。 */ + static async loginByEmail(email: string, code: string): Promise { + const req: Record = { 'email': email, 'code': code } + await AuthStore.loginAndSave('/api/v1/auth/email/login', req) + } + + /** + * 各登录通道公共收尾:无鉴权通道换双令牌并持久化。 + * 登录本身不能走带鉴权的 post()——ApiClient 对未登录一律 40100 短路, + * 登录接口会被自己的守卫拦死(鸡生蛋)。必须走无鉴权通道。 + */ + private static async loginAndSave(path: string, req: Record): Promise { + const data = await ApiClient.postWithoutAuth(path, req) await AuthStore.saveTokens(AuthStore.fromWire(data)) } diff --git a/app/entry/src/main/ets/core/HuaweiLogin.ets b/app/entry/src/main/ets/core/HuaweiLogin.ets new file mode 100644 index 0000000..4f75b4e --- /dev/null +++ b/app/entry/src/main/ets/core/HuaweiLogin.ets @@ -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,成功时 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 { + 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 { + 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 + } +} diff --git a/app/entry/src/main/ets/pages/LoginPage.ets b/app/entry/src/main/ets/pages/LoginPage.ets index e6a8d5e..ee366b4 100644 --- a/app/entry/src/main/ets/pages/LoginPage.ets +++ b/app/entry/src/main/ets/pages/LoginPage.ets @@ -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 { 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 { - if (this.logging) { + /** 华为账号登录:占位 client_id 直接提示;取消静默;其余 ApiError 走横幅。 */ + private async doHuaweiLogin(): Promise { + 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 { + 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 { + 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 { + 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 { 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, diff --git a/app/entry/src/main/module.json5 b/app/entry/src/main/module.json5 index b705182..e049cc5 100644 --- a/app/entry/src/main/module.json5 +++ b/app/entry/src/main/module.json5 @@ -23,6 +23,12 @@ ], "installationFree": false, "pages": "$profile:main_pages", + "metadata": [ + { + "name": "client_id", + "value": "6917613748100335131" + } + ], "abilities": [ { "name": "EntryAbility",