修复登录注册验证码bug

This commit is contained in:
Eamon
2026-08-14 16:30:41 +08:00
parent 4235458bea
commit 1336a550fe
9 changed files with 593 additions and 371 deletions

View File

@@ -1,11 +1,26 @@
import { callPublic } from "./base"
import { clearSession, readCookie, setSession } from "./token"
import { clearCookie, clearSession, readCookie, setSession } from "./token"
function parseUserInfo(raw: string | null): unknown {
if (!raw) return null
let decoded = raw
try {
decoded = decodeURIComponent(raw)
} catch {
decoded = raw
}
try {
return JSON.parse(decoded)
} catch {
return decoded
}
}
export async function apiLogin(props: {
logincode: string
password: string
}): Promise<{ success: boolean; message?: string }> {
const result = await callPublic("/user/ApiLogin", {
const result = await callPublic<unknown>("/user/ApiLogin", {
Logincode: props.logincode,
Password: props.password,
})
@@ -14,23 +29,15 @@ export async function apiLogin(props: {
return { success: false, message: result.message || "用户名或者密码不正确" }
}
const token = readCookie("token")
const userInfoRaw = readCookie("userInfo")
// 通过响应头下发优先读响应头cookie 兜底
const token = result.headers?.token ?? readCookie("token")
const userInfoRaw = result.headers?.userinfo ?? readCookie("userInfo")
if (!token) {
return { success: false, message: "登录成功但未获取到 token" }
}
let userInfo: unknown = userInfoRaw
if (userInfoRaw) {
try {
userInfo = JSON.parse(userInfoRaw)
} catch {
userInfo = userInfoRaw
}
}
setSession(token, userInfo)
setSession(token, parseUserInfo(userInfoRaw))
return { success: true }
}
@@ -59,5 +66,7 @@ export async function apiRegister(props: {
export async function logout() {
clearSession()
clearCookie("token")
clearCookie("userInfo")
window.location.href = "/"
}

View File

@@ -8,14 +8,31 @@ import { clearSession, getUserToken } from "./token"
type CallMode = "public" | "device" | "user"
const DEFAULT_TIMEOUT = 15_000
function captureHeaders(response: Response): Record<string, string> {
const headers: Record<string, string> = {}
response.headers.forEach((value, key) => {
headers[key.toLowerCase()] = value
})
return headers
}
async function request<R = undefined>(
endpoint: string,
init: {
method: "GET" | "POST"
body?: string
auth?: string
timeout?: number
},
): Promise<ApiResponse<R>> {
const controller = new AbortController()
const timer = setTimeout(
() => controller.abort(),
init.timeout ?? DEFAULT_TIMEOUT,
)
try {
const headers: HeadersInit = {}
if (init.body !== undefined) {
@@ -27,8 +44,10 @@ async function request<R = undefined>(
method: init.method,
headers,
body: init.body,
signal: controller.signal,
})
const responseHeaders = captureHeaders(response)
const contentType = response.headers.get("Content-Type") ?? "text/plain"
if (contentType.includes("text/plain")) {
@@ -40,7 +59,7 @@ async function request<R = undefined>(
message: text || "请求失败",
}
}
return { success: true, data: undefined as R }
return { success: true, data: undefined as R, headers: responseHeaders }
}
if (contentType.includes("application/json")) {
@@ -65,25 +84,36 @@ async function request<R = undefined>(
return {
success: true,
data: (json.Data ?? json.data ?? undefined) as R,
headers: responseHeaders,
}
}
return {
success: false,
status: response.status,
message: json.Message || "请求失败",
}
}
return { success: true, data: json }
return { success: true, data: json, headers: responseHeaders }
}
throw new Error(`无法解析响应数据: ${contentType}`)
} catch (e) {
if (controller.signal.aborted) {
return {
success: false,
status: 408,
message: "请求超时,请稍后重试",
}
}
console.error("后端请求异常:", e)
return {
success: false,
status: 500,
message: (e as Error).message || "网络错误",
}
} finally {
clearTimeout(timer)
}
}
@@ -141,40 +171,56 @@ async function call<R = undefined>(
const body = data === undefined ? undefined : JSON.stringify(data)
const run = async (): Promise<ApiResponse<R>> => {
let auth: string | undefined
if (mode === "user") {
const token = getUserToken()
if (!token) {
return {
success: false,
status: 401,
message: "未登录或会话已过期",
}
}
auth = `Bearer ${token}`
} else if (mode === "device") {
const token = await acquireDeviceToken()
if (!token) {
return {
success: false,
status: 401,
message: "设备凭证获取失败",
}
}
auth = `Bearer ${token}`
}
const maxAttempts = mode === "device" ? 2 : 1
const resp = await request<R>(endpoint, { method, body, auth })
if (!resp.success && resp.status === 401) {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
let auth: string | undefined
if (mode === "user") {
clearSession()
const token = getUserToken()
if (!token) {
return {
success: false,
status: 401,
message: "未登录或会话已过期",
}
}
auth = `Bearer ${token}`
} else if (mode === "device") {
deviceToken = null
deviceTokenExpire = 0
if (attempt > 1) {
// 上次请求 401强制作废旧 token 再重新获取
deviceToken = null
deviceTokenExpire = 0
}
const token = await acquireDeviceToken()
if (!token) {
return {
success: false,
status: 401,
message: "设备凭证获取失败",
}
}
auth = `Bearer ${token}`
}
const resp = await request<R>(endpoint, { method, body, auth })
if (!resp.success && resp.status === 401) {
if (mode === "user") {
clearSession()
return resp
}
if (mode === "device" && attempt < maxAttempts) {
continue
}
}
return resp
}
return {
success: false,
status: 401,
message: "请求未授权",
}
return resp
}
if (method === "GET" && data === undefined) {

43
src/api/linedata.ts Normal file
View File

@@ -0,0 +1,43 @@
import type { ApiResponse } from "@/lib/api"
import { callPublic } from "./base"
export type LineItem = {
city: string | number
daikuan: string | number
name: string | number
nasname: string | number
online: string | number
supply: string | number
}
export type LineData = {
count: number | string
use_count: number | string
data: LineItem[]
}
export type LineSearchResult = {
data: LineItem[]
}
export async function fetchLineData(
product: number,
): Promise<ApiResponse<LineData>> {
return await callPublic<LineData>(
`/script/linedata/display.php?product=${product}`,
undefined,
"GET",
)
}
export async function searchLineData(
productid: number,
info: string,
): Promise<ApiResponse<LineSearchResult>> {
const query = `type=0&productid=${productid}&info=${encodeURIComponent(info)}`
return await callPublic<LineSearchResult>(
`/script/linedata/search.php?${query}`,
undefined,
"GET",
)
}

View File

@@ -1,4 +1,4 @@
const AUTH_EVENT = "auth-change"
export const AUTH_EVENT = "auth-change"
const TOKEN_KEY = "auth_token"
const USER_KEY = "auth_user"
@@ -8,6 +8,11 @@ export function readCookie(name: string): string | null {
return match ? decodeURIComponent(match[1]) : null
}
export function clearCookie(name: string) {
// biome-ignore lint/suspicious/noDocumentCookie: Cookie Store API 兼容性不足,标准做法是覆盖写入
document.cookie = `${name}=; max-age=0; path=/`
}
export function getUserToken(): string | null {
return localStorage.getItem(TOKEN_KEY)
}