feat: 联调充值和提现接口

This commit is contained in:
JiaJun
2026-05-21 13:40:32 +08:00
parent 6ac42cf35e
commit 44c984d59e
51 changed files with 3830 additions and 1478 deletions

View File

@@ -5,13 +5,18 @@ import {
DEFAULT_REQUEST_TIMEOUT_MS,
} from '@/constants'
import type { AuthTokenDto } from '@/features/auth/api/types'
import { getPreferredLanguage, isSupportedLanguage } from '@/i18n'
import { ApiError } from '@/lib/api/api-error.ts'
import {
handleUnauthorizedSession,
tryRefreshAuthSession,
} from '@/lib/auth/auth-session'
import { md5 } from '@/lib/crypto/md5'
import { getAuthDeviceId, useAuthStore } from '@/store/auth'
import {
getAuthDeviceId,
getStoredAppLanguage,
useAuthStore,
} from '@/store/auth'
import type { ApiResponse } from '@/type'
type RequestOptions = Omit<Options, 'json'>
@@ -29,6 +34,16 @@ const appEnv = import.meta.env.VITE_APP_ENV
const authSecret = import.meta.env.VITE_AUTH_TOKEN_SECRET?.trim()
const shouldLogRequests = import.meta.env.VITE_ENABLE_REQUEST_LOG === 'true'
function getRequestLanguage() {
const storedLanguage = getStoredAppLanguage()
if (isSupportedLanguage(storedLanguage)) {
return storedLanguage
}
return getPreferredLanguage()
}
function normalizeApiBaseUrl(baseUrl: string | undefined) {
const candidate = baseUrl?.trim()
@@ -123,6 +138,7 @@ const apiClient = ky.create({
beforeRequest: [
({ request }) => {
request.headers.set('Accept', DEFAULT_REQUEST_ACCEPT_HEADER)
request.headers.set('lang', getRequestLanguage())
const token = useAuthStore.getState().accessToken

View File

@@ -1,15 +1,15 @@
import { create } from 'zustand'
const DEFAULT_TOAST_DURATION_MS = 3200
type NotificationType = 'success' | 'error' | 'warning' | 'info' | 'loading'
const DEFAULT_ALERT_DURATION_MS = 2600
export const NOTIFICATION_EXIT_DURATION_MS = 220
export interface NotifyOptions {
description?: string
duration?: number
}
interface NotificationToast {
interface NotificationDialog {
description?: string
duration: number
id: string
@@ -18,32 +18,134 @@ interface NotificationToast {
}
interface NotificationStoreState {
dismissToast: (id: string) => void
pushToast: (toast: NotificationToast) => void
toasts: NotificationToast[]
activeDialog: NotificationDialog | null
closingDialogId: string | null
dialogQueue: NotificationDialog[]
dismissDialog: (id?: string) => void
pushDialog: (dialog: NotificationDialog) => void
}
const toastTimers = new Map<string, number>()
const dialogTimers = new Map<string, number>()
const dialogExitTimers = new Map<string, number>()
function clearDialogTimer(id: string) {
const timerId = dialogTimers.get(id)
if (timerId) {
window.clearTimeout(timerId)
dialogTimers.delete(id)
}
}
function clearDialogExitTimer(id: string) {
const timerId = dialogExitTimers.get(id)
if (timerId) {
window.clearTimeout(timerId)
dialogExitTimers.delete(id)
}
}
function scheduleDialogDismiss(id: string, duration: number) {
clearDialogTimer(id)
if (duration <= 0) {
return
}
const timerId = window.setTimeout(() => {
useNotificationStore.getState().dismissDialog(id)
}, duration)
dialogTimers.set(id, timerId)
}
function scheduleDialogFinalizeDismiss(id: string) {
clearDialogExitTimer(id)
const timerId = window.setTimeout(() => {
useNotificationStore.setState((state) => {
if (!state.activeDialog || state.activeDialog.id !== id) {
return state
}
const [nextDialog, ...restQueue] = state.dialogQueue
if (nextDialog) {
scheduleDialogDismiss(nextDialog.id, nextDialog.duration)
}
return {
activeDialog: nextDialog ?? null,
closingDialogId: null,
dialogQueue: restQueue,
}
})
dialogExitTimers.delete(id)
}, NOTIFICATION_EXIT_DURATION_MS)
dialogExitTimers.set(id, timerId)
}
export const useNotificationStore = create<NotificationStoreState>()((set) => ({
dismissToast: (id) => {
const timerId = toastTimers.get(id)
activeDialog: null,
closingDialogId: null,
dialogQueue: [],
dismissDialog: (id) => {
set((state) => {
if (state.activeDialog) {
clearDialogTimer(state.activeDialog.id)
}
if (timerId) {
window.clearTimeout(timerId)
toastTimers.delete(id)
}
if (!state.activeDialog) {
return id
? {
closingDialogId: null,
dialogQueue: state.dialogQueue.filter(
(dialog) => dialog.id !== id,
),
}
: state
}
set((state) => ({
toasts: state.toasts.filter((toast) => toast.id !== id),
}))
if (id && state.activeDialog.id !== id) {
return {
dialogQueue: state.dialogQueue.filter((dialog) => dialog.id !== id),
}
}
if (state.closingDialogId === state.activeDialog.id) {
return state
}
scheduleDialogFinalizeDismiss(state.activeDialog.id)
return {
closingDialogId: state.activeDialog.id,
}
})
},
pushToast: (toast) => {
set((state) => ({
toasts: [...state.toasts.filter((item) => item.id !== toast.id), toast],
}))
pushDialog: (dialog) => {
set((state) => {
if (!state.activeDialog) {
clearDialogExitTimer(dialog.id)
scheduleDialogDismiss(dialog.id, dialog.duration)
return {
activeDialog: dialog,
closingDialogId: null,
}
}
return {
dialogQueue: [
...state.dialogQueue.filter((item) => item.id !== dialog.id),
dialog,
],
}
})
},
toasts: [],
}))
function createToastId() {
@@ -56,9 +158,9 @@ function showToast(
options?: NotifyOptions,
) {
const id = createToastId()
const duration = options?.duration ?? DEFAULT_TOAST_DURATION_MS
const duration = options?.duration ?? DEFAULT_ALERT_DURATION_MS
useNotificationStore.getState().pushToast({
useNotificationStore.getState().pushDialog({
description: options?.description,
duration,
id,
@@ -66,31 +168,13 @@ function showToast(
type,
})
if (duration > 0) {
const timerId = window.setTimeout(() => {
useNotificationStore.getState().dismissToast(id)
}, duration)
toastTimers.set(id, timerId)
}
return id
}
export const notify = {
dismiss(id?: string) {
if (id) {
useNotificationStore.getState().dismissToast(id)
return id
}
const { toasts } = useNotificationStore.getState()
for (const toast of toasts) {
useNotificationStore.getState().dismissToast(toast.id)
}
return null
useNotificationStore.getState().dismissDialog(id)
return id ?? null
},
error(message: string, options?: NotifyOptions) {
return showToast('error', message, options)