From 25da8a718e965c8fc080ca0bdd44d09b43ec027b Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 15 Jul 2026 14:16:14 +0700 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E7=99=BB=E5=BD=95=E5=9B=BE?= =?UTF-8?q?=E5=BD=A2=E9=AA=8C=E8=AF=81=E7=A0=81=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/login.ts | 27 +++++---- src/api/loginSms.test.ts | 66 ++++++++++++++++++++ src/api/loginSms.ts | 101 +++++++++++++++++++++++++++++++ src/i18n/modules/common/en-US.ts | 5 ++ src/i18n/modules/common/th-TH.ts | 5 ++ src/i18n/modules/common/zh-CN.ts | 5 ++ src/pages/login/index.vue | 70 +++++++++++++++++++-- 7 files changed, 263 insertions(+), 16 deletions(-) create mode 100644 src/api/loginSms.test.ts create mode 100644 src/api/loginSms.ts diff --git a/src/api/login.ts b/src/api/login.ts index e6abdb3..b1273ef 100644 --- a/src/api/login.ts +++ b/src/api/login.ts @@ -1,6 +1,7 @@ import { storeAuthTokens } from "@/constants/token"; import type { OAuthTokenResponse } from "@/constants/token"; import { getRequestContext, request, requestRaw } from "../utils/request"; +import { buildSendMobileCodeRequest } from "./loginSms"; // 获取oauth token export interface OauthTokenRequest { @@ -13,6 +14,11 @@ export interface OauthTokenRequest { [property: string]: any; } +export interface SendCodeOptions { + imageRandomStr?: string; + imageCode?: string; +} + export function buildFormUrlEncodedParams( data: Record, ): URLSearchParams { @@ -61,21 +67,18 @@ export async function oauthToken( } // 发送手机验证码 -export function sendCode(mobile: string) { +export function sendCode(mobile: string, options: SendCodeOptions = {}) { const value = (mobile ?? "").trim(); - const encoded = encodeURIComponent(value); + const requestConfig = buildSendMobileCodeRequest({ + mobile: value, + clientConfigId: getRequestContext().clientId ?? "", + imageRandomStr: options.imageRandomStr, + imageCode: options.imageCode, + }); return request( - { - url: `/admin/platformUser/sendMobileCode/${encoded}`, - method: "get", - }, - { - skipAuth: true, - headers: { - clientConfigId: getRequestContext().clientId ?? "", - }, - }, + requestConfig.config, + requestConfig.options, ); } diff --git a/src/api/loginSms.test.ts b/src/api/loginSms.test.ts new file mode 100644 index 0000000..2bd4750 --- /dev/null +++ b/src/api/loginSms.test.ts @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + buildCaptchaImageUrl, + buildSendMobileCodeRequest, + resolveLoginErrorMessage, +} from "./loginSms.ts"; + +describe("login sms api helpers", () => { + it("builds send-code request with image captcha params and clientConfigId header", () => { + const result = buildSendMobileCodeRequest({ + mobile: "+8613800000000", + clientConfigId: "6", + imageRandomStr: "captcha-random", + imageCode: "42", + }); + + assert.deepEqual(result.config, { + url: "/admin/platformUser/sendMobileCode/%2B8613800000000", + method: "get", + params: { + imageRandomStr: "captcha-random", + imageCode: "42", + }, + }); + assert.deepEqual(result.options, { + skipAuth: true, + headers: { + clientConfigId: "6", + }, + }); + }); + + it("builds captcha image url from api base url", () => { + assert.equal( + buildCaptchaImageUrl("captcha random", { + baseURL: "/ingress/", + cacheBust: 123, + }), + "/ingress/auth/code/image?randomStr=captcha+random&_t=123", + ); + }); + + it("uses backend business error message before generic fallback", () => { + assert.equal( + resolveLoginErrorMessage( + { + kind: "business", + message: "图形验证码不合法", + }, + "网络错误", + ), + "图形验证码不合法", + ); + assert.equal( + resolveLoginErrorMessage( + { + kind: "network", + message: "Network Error", + }, + "网络错误", + ), + "网络错误", + ); + }); +}); diff --git a/src/api/loginSms.ts b/src/api/loginSms.ts new file mode 100644 index 0000000..a4a639e --- /dev/null +++ b/src/api/loginSms.ts @@ -0,0 +1,101 @@ +export interface SendMobileCodeRequestParams { + mobile: string; + clientConfigId: string; + imageRandomStr?: string; + imageCode?: string; +} + +export interface CaptchaImageUrlOptions { + baseURL?: string; + cacheBust?: number | string; +} + +function trimEndSlash(value: string): string { + return value.replace(/\/+$/, ""); +} + +function resolveApiBaseURL(): string { + const env = (import.meta as ImportMeta & { env?: Record }).env; + const value = env?.VITE_API_BASE_URL; + return typeof value === "string" ? value.trim() : ""; +} + +function appendIfPresent( + params: Record, + key: string, + value?: string, +) { + const normalized = value?.trim(); + if (normalized) { + params[key] = normalized; + } +} + +export function createCaptchaRandomStr(): string { + if (globalThis.crypto?.randomUUID) { + return globalThis.crypto.randomUUID().replace(/-/g, ""); + } + + return `${Date.now()}${Math.random().toString(36).slice(2, 10)}`; +} + +export function buildCaptchaImageUrl( + randomStr: string, + options: CaptchaImageUrlOptions = {}, +): string { + const query = new URLSearchParams({ + randomStr, + _t: String(options.cacheBust ?? Date.now()), + }); + const baseURL = trimEndSlash(options.baseURL ?? resolveApiBaseURL()); + + return `${baseURL}/auth/code/image?${query.toString()}`; +} + +export function buildSendMobileCodeRequest({ + mobile, + clientConfigId, + imageRandomStr, + imageCode, +}: SendMobileCodeRequestParams) { + const params: Record = {}; + appendIfPresent(params, "imageRandomStr", imageRandomStr); + appendIfPresent(params, "imageCode", imageCode); + + return { + config: { + url: `/admin/platformUser/sendMobileCode/${encodeURIComponent( + mobile.trim(), + )}`, + method: "get" as const, + params: Object.keys(params).length > 0 ? params : undefined, + }, + options: { + skipAuth: true, + headers: { + clientConfigId, + }, + }, + }; +} + +export function resolveLoginErrorMessage( + error: unknown, + fallback: string, +): string { + if (!error || typeof error !== "object") { + return fallback; + } + + const value = error as Record; + const message = value.message; + if ( + value.kind === "business" && + typeof message === "string" && + message.trim() + ) { + return message; + } + + return fallback; +} diff --git a/src/i18n/modules/common/en-US.ts b/src/i18n/modules/common/en-US.ts index adae98a..3f06eb9 100644 --- a/src/i18n/modules/common/en-US.ts +++ b/src/i18n/modules/common/en-US.ts @@ -82,23 +82,28 @@ export default { fields: { country: "Country/Code", phone: "Phone", + imageCode: "Captcha", code: "Code", }, placeholders: { selectCountry: "Select country code", phone: "Enter phone number", + imageCode: "Enter captcha", code: "Enter verification code", }, dividersText: "Or", actions: { sendCode: "Send code", + refreshImageCode: "Refresh captcha", login: "Sign in", }, errors: { missingPhone: "Please enter your phone number", + missingImageCode: "Please enter the captcha", missingCode: "Please enter the verification code", }, tips: { + codeSent: "Verification code sent", smsApiMissing: "SMS sending API is not connected", }, }, diff --git a/src/i18n/modules/common/th-TH.ts b/src/i18n/modules/common/th-TH.ts index a3055c3..0583f4c 100644 --- a/src/i18n/modules/common/th-TH.ts +++ b/src/i18n/modules/common/th-TH.ts @@ -82,23 +82,28 @@ export default { fields: { country: "ประเทศ/รหัส", phone: "โทรศัพท์", + imageCode: "แคปต์ชา", code: "รหัสยืนยัน", }, placeholders: { selectCountry: "เลือกรหัสประเทศ", phone: "กรอกหมายเลขโทรศัพท์", + imageCode: "กรอกแคปต์ชา", code: "กรอกรหัสยืนยัน", }, dividersText: "หรือ", actions: { sendCode: "ส่งรหัส", + refreshImageCode: "รีเฟรชแคปต์ชา", login: "เข้าสู่ระบบ", }, errors: { missingPhone: "กรอกหมายเลขโทรศัพท์", + missingImageCode: "กรอกแคปต์ชา", missingCode: "กรอกรหัสยืนยัน", }, tips: { + codeSent: "ส่งรหัสยืนยันแล้ว", smsApiMissing: "ยังไม่ได้เชื่อมต่อ API สำหรับส่ง SMS", }, }, diff --git a/src/i18n/modules/common/zh-CN.ts b/src/i18n/modules/common/zh-CN.ts index 571f5d8..70aa305 100644 --- a/src/i18n/modules/common/zh-CN.ts +++ b/src/i18n/modules/common/zh-CN.ts @@ -82,23 +82,28 @@ export default { fields: { country: "国家/区号", phone: "手机号", + imageCode: "图形验证码", code: "验证码", }, placeholders: { selectCountry: "请选择国家/区号", phone: "请输入手机号", + imageCode: "请输入图形验证码", code: "请输入验证码", }, dividersText: "或", actions: { sendCode: "获取验证码", + refreshImageCode: "刷新图形验证码", login: "登录", }, errors: { missingPhone: "请输入手机号", + missingImageCode: "请输入图形验证码", missingCode: "请输入验证码", }, tips: { + codeSent: "验证码已发送", smsApiMissing: "验证码发送接口未接入", }, }, diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue index 0c45b4e..023cbdd 100644 --- a/src/pages/login/index.vue +++ b/src/pages/login/index.vue @@ -16,11 +16,24 @@ + +