优化前端认证与类型管理
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
import type { ApiResponse, PaginatedResult } from './types'
|
||||
|
||||
export interface AdminAuditLog {
|
||||
id: number
|
||||
@@ -25,12 +25,6 @@ export interface AdminAuditQuery {
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchAdminAuditLogs(query: AdminAuditQuery = {}) {
|
||||
const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== undefined))
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminAuditLog>>>('/admin/audit-logs', { params })
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
import { getRefreshToken, setAuthTokens } from '@/utils/authStorage'
|
||||
import type { UserStatus } from '@/types/status'
|
||||
|
||||
export interface AdminUser {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
status: string
|
||||
status: UserStatus
|
||||
last_login_at?: string
|
||||
}
|
||||
|
||||
@@ -28,12 +31,6 @@ export interface AdminCaptcha {
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchAdminCaptcha() {
|
||||
const { data } = await apiClient.get<ApiResponse<AdminCaptcha>>('/admin/auth/captcha')
|
||||
return data.data
|
||||
@@ -61,8 +58,9 @@ export async function logoutAdmin() {
|
||||
|
||||
/** Manually refresh admin token (uses raw axios to avoid interceptor recursion) */
|
||||
export async function refreshAdminSession() {
|
||||
const refreshToken = localStorage.getItem('admin_refresh_token')
|
||||
const refreshToken = getRefreshToken('admin')
|
||||
if (!refreshToken) throw new Error('no refresh token')
|
||||
const { data } = await axios.post('/api/admin/auth/refresh', { refresh_token: refreshToken })
|
||||
return data.data as AdminTokenPair
|
||||
const { data } = await axios.post<ApiResponse<AdminTokenPair>>('/api/admin/auth/refresh', { refresh_token: refreshToken })
|
||||
setAuthTokens('admin', data.data)
|
||||
return data.data
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
import type { DisputeStatus, OrderStatus } from '@/types/status'
|
||||
|
||||
export interface DashboardMetrics {
|
||||
total_users: number
|
||||
@@ -24,7 +26,7 @@ export interface DashboardRecentOrder {
|
||||
title: string
|
||||
renter_id: number
|
||||
owner_id: number
|
||||
status: string
|
||||
status: OrderStatus
|
||||
rent_amount: number
|
||||
deposit_amount: number
|
||||
created_at: string
|
||||
@@ -36,7 +38,7 @@ export interface DashboardRecentDispute {
|
||||
order_no: string
|
||||
title: string
|
||||
type: string
|
||||
status: string
|
||||
status: DisputeStatus
|
||||
initiator_id: number
|
||||
created_at: string
|
||||
}
|
||||
@@ -49,12 +51,6 @@ export interface AdminDashboard {
|
||||
generated_at: string
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchAdminDashboard() {
|
||||
const { data } = await apiClient.get<ApiResponse<AdminDashboard>>('/admin/dashboard')
|
||||
return data.data
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
import type { ApiResponse, PaginatedResult } from './types'
|
||||
import type { RealnameStatusValue, RiskStatus, UserStatus } from '@/types/status'
|
||||
|
||||
export interface AdminUserItem {
|
||||
id: number
|
||||
phone: string
|
||||
nickname: string
|
||||
realname_status: string
|
||||
risk_status: string
|
||||
realname_status: RealnameStatusValue
|
||||
risk_status: RiskStatus
|
||||
credit_score: number
|
||||
status: string
|
||||
status: UserStatus
|
||||
order_count: number
|
||||
listing_count: number
|
||||
dispute_count: number
|
||||
@@ -18,12 +19,6 @@ export interface AdminUserItem {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchAdminUsers(page = 1, pageSize = 20) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminUserItem>>>('/admin/users', {
|
||||
params: { page, page_size: pageSize },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
import type { ApiResponse, PaginatedResult } from './types'
|
||||
import type { BalanceType, LedgerDirection } from '@/types/status'
|
||||
|
||||
export interface AdminWalletLedger {
|
||||
id: number
|
||||
@@ -10,10 +11,10 @@ export interface AdminWalletLedger {
|
||||
user_nickname: string
|
||||
order_id?: number
|
||||
order_no: string
|
||||
direction: string
|
||||
direction: LedgerDirection
|
||||
amount: number
|
||||
balance_after: number
|
||||
balance_type: string
|
||||
balance_type: BalanceType
|
||||
biz_type: string
|
||||
biz_no: string
|
||||
remark: string
|
||||
@@ -28,12 +29,6 @@ export interface AdminWalletLedgerQuery {
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchAdminWalletLedger(query: AdminWalletLedgerQuery = {}) {
|
||||
const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== undefined))
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<AdminWalletLedger>>>('/admin/wallet/ledger', { params })
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
import type { RealnameStatusValue, RiskStatus, UserStatus } from '@/types/status'
|
||||
|
||||
export interface AuthUser {
|
||||
id: number
|
||||
phone: string
|
||||
nickname: string
|
||||
avatar_url: string
|
||||
realname_status: string
|
||||
risk_status: string
|
||||
realname_status: RealnameStatusValue
|
||||
risk_status: RiskStatus
|
||||
credit_score: number
|
||||
status: string
|
||||
status: UserStatus
|
||||
}
|
||||
|
||||
export interface TokenPair {
|
||||
@@ -23,12 +25,6 @@ export interface LoginData {
|
||||
tokens: TokenPair
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function sendSmsCode(phone: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<{ phone: string; expires_in: number }>>('/auth/sms/send', {
|
||||
phone,
|
||||
@@ -57,4 +53,4 @@ export async function refreshUserToken(refreshToken: string) {
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
}
|
||||
|
||||
+98
-87
@@ -1,131 +1,142 @@
|
||||
import axios, { type InternalAxiosRequestConfig } from 'axios'
|
||||
import axios, { type AxiosResponse, type InternalAxiosRequestConfig } from 'axios'
|
||||
|
||||
import {
|
||||
clearAuthStorage,
|
||||
getAccessToken,
|
||||
getLoginPath,
|
||||
getRefreshToken,
|
||||
setAuthTokens,
|
||||
type AuthScope,
|
||||
} from '@/utils/authStorage'
|
||||
import type { ApiResponse } from './types'
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
// ---- refresh retry lock ----
|
||||
let isRefreshing = false
|
||||
let pendingRequests: Array<(token: string) => void> = []
|
||||
|
||||
function subscribePendingRequests(token: string) {
|
||||
pendingRequests.forEach((cb) => cb(token))
|
||||
pendingRequests.length = 0
|
||||
export async function unwrapData<T>(request: Promise<AxiosResponse<ApiResponse<T>>>) {
|
||||
const { data } = await request
|
||||
return data.data
|
||||
}
|
||||
|
||||
function addPendingRequest(callback: (token: string) => void) {
|
||||
pendingRequests.push(callback)
|
||||
type RetryRequest = {
|
||||
resolve: (token: string) => void
|
||||
reject: (error: unknown) => void
|
||||
}
|
||||
|
||||
async function refreshTokenAndRetry(isAdmin = false): Promise<string> {
|
||||
const refreshTokenKey = isAdmin ? 'admin_refresh_token' : 'refresh_token'
|
||||
const refreshToken = localStorage.getItem(refreshTokenKey)
|
||||
if (!refreshToken) {
|
||||
throw new Error('no refresh token')
|
||||
}
|
||||
const endpoint = isAdmin ? '/api/admin/auth/refresh' : '/api/auth/refresh'
|
||||
// use raw axios (not apiClient) to avoid interceptor recursion
|
||||
type RefreshState = {
|
||||
refreshing: boolean
|
||||
pendingRequests: RetryRequest[]
|
||||
}
|
||||
|
||||
type RetriableRequestConfig = InternalAxiosRequestConfig & { _retry?: boolean }
|
||||
|
||||
const refreshStates: Record<AuthScope, RefreshState> = {
|
||||
user: {
|
||||
refreshing: false,
|
||||
pendingRequests: [],
|
||||
},
|
||||
admin: {
|
||||
refreshing: false,
|
||||
pendingRequests: [],
|
||||
},
|
||||
}
|
||||
|
||||
function resolvePendingRequests(scope: AuthScope, token: string) {
|
||||
const pendingRequests = refreshStates[scope].pendingRequests.splice(0)
|
||||
pendingRequests.forEach(({ resolve }) => resolve(token))
|
||||
}
|
||||
|
||||
function rejectPendingRequests(scope: AuthScope, error: unknown) {
|
||||
const pendingRequests = refreshStates[scope].pendingRequests.splice(0)
|
||||
pendingRequests.forEach(({ reject }) => reject(error))
|
||||
}
|
||||
|
||||
async function refreshToken(scope: AuthScope): Promise<string> {
|
||||
const refreshToken = getRefreshToken(scope)
|
||||
if (!refreshToken) throw new Error('no refresh token')
|
||||
|
||||
const endpoint = scope === 'admin' ? '/api/admin/auth/refresh' : '/api/auth/refresh'
|
||||
const { data } = await axios.post(endpoint, { refresh_token: refreshToken })
|
||||
const newAccessToken = data.data.access_token
|
||||
const newRefreshToken = data.data.refresh_token
|
||||
const accessKey = isAdmin ? 'admin_access_token' : 'access_token'
|
||||
localStorage.setItem(accessKey, newAccessToken)
|
||||
localStorage.setItem(refreshTokenKey, newRefreshToken)
|
||||
return newAccessToken
|
||||
const tokens = {
|
||||
access_token: data.data.access_token,
|
||||
refresh_token: data.data.refresh_token,
|
||||
}
|
||||
setAuthTokens(scope, tokens)
|
||||
return tokens.access_token
|
||||
}
|
||||
|
||||
// clear user tokens
|
||||
function clearUserTokens() {
|
||||
;['access_token', 'refresh_token', 'user_id', 'phone', 'nickname', 'avatar_url', 'realname_status']
|
||||
.forEach((k) => localStorage.removeItem(k))
|
||||
function getRequestScope(url = ''): AuthScope {
|
||||
return url.startsWith('/admin') ? 'admin' : 'user'
|
||||
}
|
||||
|
||||
// clear admin tokens
|
||||
function clearAdminTokens() {
|
||||
;['admin_access_token', 'admin_refresh_token', 'admin_id', 'admin_username']
|
||||
.forEach((k) => localStorage.removeItem(k))
|
||||
function redirectToLogin(scope: AuthScope) {
|
||||
clearAuthStorage(scope)
|
||||
const currentPath = window.location.pathname + window.location.search
|
||||
const loginPath = getLoginPath(scope, currentPath)
|
||||
if (currentPath.startsWith(loginPath)) return
|
||||
|
||||
if (scope === 'admin') {
|
||||
window.location.assign(loginPath)
|
||||
return
|
||||
}
|
||||
|
||||
window.location.assign(`${loginPath}?redirect=${encodeURIComponent(currentPath)}`)
|
||||
}
|
||||
|
||||
function isRefreshRequest(url = '') {
|
||||
return url.endsWith('/auth/refresh') || url.endsWith('/admin/auth/refresh')
|
||||
}
|
||||
|
||||
// ---- Request interceptor ----
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const url = config.url || ''
|
||||
const tokenKey = url.startsWith('/admin') ? 'admin_access_token' : 'access_token'
|
||||
const token = localStorage.getItem(tokenKey)
|
||||
const scope = getRequestScope(config.url || '')
|
||||
const token = getAccessToken(scope)
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// ---- Response interceptor ----
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
|
||||
|
||||
if (error?.response?.status !== 401 || originalRequest._retry) {
|
||||
const originalRequest = error.config as RetriableRequestConfig | undefined
|
||||
if (!originalRequest || error?.response?.status !== 401 || originalRequest._retry) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
const requestUrl = originalRequest.url || ''
|
||||
const isAdminRequest = requestUrl.startsWith('/admin')
|
||||
|
||||
// exclude refresh endpoints themselves to avoid dead loop
|
||||
if (requestUrl.endsWith('/auth/refresh') || requestUrl.endsWith('/admin/auth/refresh')) {
|
||||
if (isAdminRequest) clearAdminTokens()
|
||||
else clearUserTokens()
|
||||
if (isAdminRequest && !window.location.pathname.startsWith('/admin/login')) {
|
||||
window.location.assign('/admin/login')
|
||||
} else if (!isAdminRequest) {
|
||||
const currentPath = window.location.pathname + window.location.search
|
||||
if (!currentPath.startsWith('/m/login') && !currentPath.startsWith('/login')) {
|
||||
const redirect = encodeURIComponent(currentPath)
|
||||
const loginPath = currentPath.startsWith('/m') ? '/m/login' : '/login'
|
||||
window.location.assign(`${loginPath}?redirect=${redirect}`)
|
||||
}
|
||||
}
|
||||
const scope = getRequestScope(requestUrl)
|
||||
if (isRefreshRequest(requestUrl)) {
|
||||
redirectToLogin(scope)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
// if already refreshing, queue up
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
addPendingRequest((newToken: string) => {
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
resolve(apiClient(originalRequest))
|
||||
})
|
||||
const state = refreshStates[scope]
|
||||
if (state.refreshing) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
state.pendingRequests.push({ resolve, reject })
|
||||
}).then((newToken) => {
|
||||
originalRequest._retry = true
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
return apiClient(originalRequest)
|
||||
})
|
||||
}
|
||||
|
||||
// attempt refresh
|
||||
isRefreshing = true
|
||||
state.refreshing = true
|
||||
try {
|
||||
const newToken = await refreshTokenAndRetry(isAdminRequest)
|
||||
subscribePendingRequests(newToken)
|
||||
const newToken = await refreshToken(scope)
|
||||
resolvePendingRequests(scope, newToken)
|
||||
originalRequest._retry = true
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
return apiClient(originalRequest)
|
||||
} catch {
|
||||
// refresh failed, clear tokens and redirect to login
|
||||
subscribePendingRequests('') // let queued requests fail
|
||||
if (isAdminRequest) {
|
||||
clearAdminTokens()
|
||||
if (!window.location.pathname.startsWith('/admin/login')) {
|
||||
window.location.assign('/admin/login')
|
||||
}
|
||||
} else {
|
||||
clearUserTokens()
|
||||
const currentPath = window.location.pathname + window.location.search
|
||||
if (!currentPath.startsWith('/m/login') && !currentPath.startsWith('/login')) {
|
||||
const redirect = encodeURIComponent(currentPath)
|
||||
const loginPath = currentPath.startsWith('/m') ? '/m/login' : '/login'
|
||||
window.location.assign(`${loginPath}?redirect=${redirect}`)
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
} catch (refreshError) {
|
||||
rejectPendingRequests(scope, refreshError)
|
||||
redirectToLogin(scope)
|
||||
return Promise.reject(refreshError)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
state.refreshing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
import type { ApiResponse, PaginatedResult } from './types'
|
||||
import type { DisputeStatus } from '@/types/status'
|
||||
|
||||
export interface Dispute {
|
||||
id: number
|
||||
@@ -10,7 +11,7 @@ export interface Dispute {
|
||||
initiator_id: number
|
||||
target_user_id: number
|
||||
type: string
|
||||
status: string
|
||||
status: DisputeStatus
|
||||
description: string
|
||||
evidence_urls?: string[]
|
||||
arbitration_result: string
|
||||
@@ -21,12 +22,6 @@ export interface Dispute {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function createDispute(orderId: number, payload: { type: string; description: string; evidence_urls?: string[] }) {
|
||||
const { data } = await apiClient.post<ApiResponse<Dispute>>(`/orders/${orderId}/dispute`, payload)
|
||||
return data.data
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
|
||||
export interface UploadedFile {
|
||||
object_key: string
|
||||
@@ -8,12 +9,6 @@ export interface UploadedFile {
|
||||
size: number
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function uploadFile(file: File, scene: string) {
|
||||
const form = new FormData()
|
||||
form.append('file', file)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
import {
|
||||
mergeListingPublishOptions,
|
||||
type ListingPublishOptions,
|
||||
@@ -19,12 +20,6 @@ export interface MobileHomeConfig {
|
||||
publish_options: ListingPublishOptions
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export const defaultHomeAnnouncements = [
|
||||
'平台担保交易,拒绝私下转账/共享验证码,交接全程留痕。',
|
||||
'优先推荐同地区账号,减少异地登录保护触发。',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
|
||||
export type ChargeMode = '赠送' | '收费'
|
||||
|
||||
@@ -105,12 +106,6 @@ export interface ListingPublishOptions {
|
||||
ratio_config: PublishRatioConfig
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export const emptyListingPublishOptions: ListingPublishOptions = {
|
||||
server_options: [],
|
||||
face_options: [],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
import type { ListingReviewStatus, ListingStatus } from '@/types/status'
|
||||
|
||||
export interface Listing {
|
||||
id: number
|
||||
@@ -20,8 +22,8 @@ export interface Listing {
|
||||
deposit_amount: number
|
||||
is_accelerated_sale?: boolean
|
||||
in_transaction: boolean
|
||||
status: string
|
||||
review_status: string
|
||||
status: ListingStatus
|
||||
review_status: ListingReviewStatus
|
||||
review_reason: string
|
||||
published_at?: string
|
||||
created_at: string
|
||||
@@ -41,12 +43,6 @@ export interface ListingPayload {
|
||||
deposit_amount: number
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchListings() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: Listing[] }>>('/listings')
|
||||
return data.data.items
|
||||
@@ -84,8 +80,8 @@ export async function fetchPendingReviewListings() {
|
||||
|
||||
export interface AdminListingQuery {
|
||||
owner_id?: string
|
||||
status?: string
|
||||
review_status?: string
|
||||
status?: ListingStatus | ''
|
||||
review_status?: ListingReviewStatus | ''
|
||||
limit?: number
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
import type { ApiResponse, PaginatedResult } from './types'
|
||||
|
||||
export interface NotificationItem {
|
||||
id: number
|
||||
@@ -14,12 +14,6 @@ export interface NotificationItem {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchNotifications(page = 1, pageSize = 20) {
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<NotificationItem>>>('/notifications', {
|
||||
params: { page, page_size: pageSize },
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
import type { HandoffStatus, OrderStatus, SettlementStatus } from '@/types/status'
|
||||
|
||||
export interface Order {
|
||||
id: number
|
||||
@@ -19,9 +21,9 @@ export interface Order {
|
||||
deposit_amount: number
|
||||
platform_fee: number
|
||||
account_snapshot?: Record<string, unknown>
|
||||
status: string
|
||||
handoff_status: string
|
||||
settlement_status: string
|
||||
status: OrderStatus
|
||||
handoff_status: HandoffStatus
|
||||
settlement_status: SettlementStatus
|
||||
checkout?: Checkout
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -31,7 +33,7 @@ export interface Checkout {
|
||||
id: number
|
||||
order_id: number
|
||||
initiated_by: number
|
||||
status: string
|
||||
status: SettlementStatus
|
||||
rent_amount: number
|
||||
owner_rent_amount: number
|
||||
platform_fee: number
|
||||
@@ -81,19 +83,6 @@ export interface CounterCheckoutPayload {
|
||||
evidence_urls: string[]
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export async function createOrder(listingId: number) {
|
||||
const { data } = await apiClient.post<ApiResponse<Order>>('/orders', {
|
||||
listing_id: listingId,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
import type { RealnameStatusValue } from '@/types/status'
|
||||
|
||||
export interface RealnameStatus {
|
||||
status: string
|
||||
status: RealnameStatusValue
|
||||
provider?: string
|
||||
provider_order_no?: string
|
||||
masked_name?: string
|
||||
@@ -10,12 +12,6 @@ export interface RealnameStatus {
|
||||
fail_reason?: string
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function startRealname(name: string, idNo: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<RealnameStatus>>('/realname/start', {
|
||||
name,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiClient } from './client'
|
||||
import type { ApiResponse } from './types'
|
||||
|
||||
export interface SystemConfig {
|
||||
id: number
|
||||
@@ -10,12 +11,6 @@ export interface SystemConfig {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchSystemConfigs() {
|
||||
const { data } = await apiClient.get<ApiResponse<{ items: SystemConfig[] }>>('/admin/system-configs')
|
||||
return data.data.items
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { apiClient } from './client'
|
||||
|
||||
import { type PaginatedResult } from './orders'
|
||||
import type { ApiResponse, PaginatedResult } from './types'
|
||||
import type { BalanceType, LedgerDirection, WalletStatus } from '@/types/status'
|
||||
|
||||
export interface WalletAccount {
|
||||
user_id: number
|
||||
available_balance: number
|
||||
frozen_balance: number
|
||||
status: string
|
||||
status: WalletStatus
|
||||
}
|
||||
|
||||
export interface WalletLedger {
|
||||
@@ -14,22 +15,16 @@ export interface WalletLedger {
|
||||
ledger_no: string
|
||||
user_id: number
|
||||
order_id?: number
|
||||
direction: string
|
||||
direction: LedgerDirection
|
||||
amount: number
|
||||
balance_after: number
|
||||
balance_type: string
|
||||
balance_type: BalanceType
|
||||
biz_type: string
|
||||
biz_no: string
|
||||
remark: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export async function fetchWalletBalance() {
|
||||
const { data } = await apiClient.get<ApiResponse<WalletAccount>>('/wallet/balance')
|
||||
return data.data
|
||||
|
||||
@@ -64,7 +64,9 @@ export function readPublishDraft(draftKey: string) {
|
||||
}
|
||||
|
||||
export function writePublishDraft(draftKey: string, draft: PublishDraft) {
|
||||
localStorage.setItem(draftKey, JSON.stringify(draft))
|
||||
const nextValue = JSON.stringify(draft)
|
||||
if (localStorage.getItem(draftKey) === nextValue) return
|
||||
localStorage.setItem(draftKey, nextValue)
|
||||
}
|
||||
|
||||
export function removePublishDraft(draftKey: string) {
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
import type { PublishForm } from '@/types/publish'
|
||||
import { commonOnlineTimes, dailyLossOptions, formatNumber, roundRatio } from '@/utils/pricing'
|
||||
|
||||
const draftSaveDelay = 400
|
||||
|
||||
interface UsePublishFormOptions {
|
||||
draftKey: string
|
||||
submitSuccessPath: string
|
||||
@@ -51,6 +53,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
const screenshotFiles = reactive<Record<string, string>>({})
|
||||
const screenshotPreviews = reactive<Record<string, string>>({})
|
||||
const selectedSkins = ref<string[]>([])
|
||||
let draftSaveTimer: number | undefined
|
||||
|
||||
const pricing = usePricingCalculator({
|
||||
form,
|
||||
@@ -124,8 +127,34 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
}
|
||||
|
||||
function saveDraft(saveOptions: { force?: boolean } = {}) {
|
||||
if (suppressDraftSave.value) return
|
||||
if (suppressDraftSave.value) {
|
||||
clearDraftSaveTimer()
|
||||
return
|
||||
}
|
||||
if (!saveOptions.force && !draftReady.value) return
|
||||
if (!saveOptions.force) {
|
||||
scheduleDraftSave()
|
||||
return
|
||||
}
|
||||
clearDraftSaveTimer()
|
||||
writeDraft()
|
||||
}
|
||||
|
||||
function scheduleDraftSave() {
|
||||
clearDraftSaveTimer()
|
||||
draftSaveTimer = window.setTimeout(() => {
|
||||
draftSaveTimer = undefined
|
||||
writeDraft()
|
||||
}, draftSaveDelay)
|
||||
}
|
||||
|
||||
function clearDraftSaveTimer() {
|
||||
if (draftSaveTimer === undefined) return
|
||||
window.clearTimeout(draftSaveTimer)
|
||||
draftSaveTimer = undefined
|
||||
}
|
||||
|
||||
function writeDraft() {
|
||||
writePublishDraft(
|
||||
options.draftKey,
|
||||
buildPublishDraft({
|
||||
@@ -178,6 +207,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
return
|
||||
}
|
||||
suppressDraftSave.value = true
|
||||
clearDraftSaveTimer()
|
||||
resetDraftState()
|
||||
removePublishDraft(options.draftKey)
|
||||
options.notifySuccess('已重置')
|
||||
@@ -349,6 +379,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
})
|
||||
removePublishDraft(options.draftKey)
|
||||
suppressDraftSave.value = true
|
||||
clearDraftSaveTimer()
|
||||
options.notifySuccess(
|
||||
listing.status === 'published' && listing.review_status === 'approved'
|
||||
? '发布成功,已上架'
|
||||
|
||||
+11
-1
@@ -8,7 +8,17 @@ import { createApp } from "vue";
|
||||
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { useAdminSessionStore } from "./stores/adminSession";
|
||||
import { useSessionStore } from "./stores/session";
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(createPinia()).use(router).use(ElementPlus);
|
||||
const pinia = createPinia();
|
||||
|
||||
app.use(pinia).use(router).use(ElementPlus);
|
||||
|
||||
window.addEventListener("auth-storage-changed", () => {
|
||||
useSessionStore(pinia).syncFromStorage();
|
||||
useAdminSessionStore(pinia).syncFromStorage();
|
||||
});
|
||||
|
||||
router.isReady().then(() => app.mount("#app"));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { getAccessToken, getLoginPath } from '@/utils/authStorage'
|
||||
import { accountRoutes } from './accountRoutes'
|
||||
import { adminRoutes } from './adminRoutes'
|
||||
import { isMobileHost, toMobilePath } from './mobileHost'
|
||||
@@ -26,16 +28,17 @@ router.beforeEach(async (to) => {
|
||||
|
||||
if (to.meta.requiresAuth) {
|
||||
const session = useSessionStore()
|
||||
const hasToken = !!localStorage.getItem('access_token')
|
||||
session.syncFromStorage()
|
||||
const hasToken = !!session.token
|
||||
if (!hasToken) {
|
||||
const loginPath = to.path.startsWith('/m') ? '/m/login' : '/login'
|
||||
const loginPath = getLoginPath('user', to.path)
|
||||
return { path: loginPath, query: { redirect: to.fullPath } }
|
||||
}
|
||||
if (!session.phone) {
|
||||
try {
|
||||
await session.loadMe()
|
||||
} catch {
|
||||
const loginPath = to.path.startsWith('/m') ? '/m/login' : '/login'
|
||||
const loginPath = getLoginPath('user', to.path)
|
||||
return { path: loginPath, query: { redirect: to.fullPath } }
|
||||
}
|
||||
}
|
||||
@@ -43,14 +46,15 @@ router.beforeEach(async (to) => {
|
||||
|
||||
if (to.meta.requiresRealname) {
|
||||
const session = useSessionStore()
|
||||
const loginPath = to.path.startsWith('/m') ? '/m/login' : '/login'
|
||||
session.syncFromStorage()
|
||||
const loginPath = getLoginPath('user', to.path)
|
||||
const realnamePath = to.path.startsWith('/m') ? '/m/realname' : '/realname'
|
||||
|
||||
if (session.realnameStatus !== 'verified') {
|
||||
try {
|
||||
await session.loadMe()
|
||||
} catch {
|
||||
if (!localStorage.getItem('access_token')) {
|
||||
if (!getAccessToken('user')) {
|
||||
return { path: loginPath, query: { redirect: to.fullPath } }
|
||||
}
|
||||
}
|
||||
@@ -61,12 +65,13 @@ router.beforeEach(async (to) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (to.path === '/admin/login' && localStorage.getItem('admin_access_token')) {
|
||||
const adminSession = useAdminSessionStore()
|
||||
adminSession.syncFromStorage()
|
||||
if (to.path === '/admin/login' && adminSession.token) {
|
||||
return '/admin/dashboard'
|
||||
}
|
||||
if (to.meta.requiresAdmin) {
|
||||
const token = localStorage.getItem('admin_access_token')
|
||||
if (!token) {
|
||||
if (!adminSession.token) {
|
||||
return '/admin/login'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { fetchAdminMe, loginAdmin, type AdminUser } from '@/api/adminAuth'
|
||||
import { clearAuthStorage, getAccessToken, getRefreshToken, setAuthTokens } from '@/utils/authStorage'
|
||||
|
||||
export const useAdminSessionStore = defineStore('adminSession', {
|
||||
state: () => ({
|
||||
token: localStorage.getItem('admin_access_token') || '',
|
||||
refreshToken: localStorage.getItem('admin_refresh_token') || '',
|
||||
token: getAccessToken('admin'),
|
||||
refreshToken: getRefreshToken('admin'),
|
||||
adminId: Number(localStorage.getItem('admin_id') || 0),
|
||||
username: localStorage.getItem('admin_username') || '',
|
||||
nickname: '',
|
||||
@@ -27,16 +28,21 @@ export const useAdminSessionStore = defineStore('adminSession', {
|
||||
this.adminId = 0
|
||||
this.username = ''
|
||||
this.nickname = ''
|
||||
localStorage.removeItem('admin_access_token')
|
||||
localStorage.removeItem('admin_refresh_token')
|
||||
localStorage.removeItem('admin_id')
|
||||
localStorage.removeItem('admin_username')
|
||||
clearAuthStorage('admin')
|
||||
},
|
||||
syncFromStorage() {
|
||||
this.token = getAccessToken('admin')
|
||||
this.refreshToken = getRefreshToken('admin')
|
||||
this.adminId = Number(localStorage.getItem('admin_id') || 0)
|
||||
this.username = localStorage.getItem('admin_username') || ''
|
||||
},
|
||||
applySession(admin: AdminUser, accessToken: string, refreshToken: string) {
|
||||
this.token = accessToken
|
||||
this.refreshToken = refreshToken
|
||||
localStorage.setItem('admin_access_token', accessToken)
|
||||
localStorage.setItem('admin_refresh_token', refreshToken)
|
||||
setAuthTokens('admin', {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
})
|
||||
this.applyAdmin(admin)
|
||||
},
|
||||
applyAdmin(admin: AdminUser) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
import { fetchMe, loginWithSms, updateMe, type AuthUser } from "@/api/auth";
|
||||
import { clearAuthStorage, getAccessToken, getRefreshToken, setAuthTokens } from "@/utils/authStorage";
|
||||
|
||||
export const useSessionStore = defineStore("session", {
|
||||
state: () => ({
|
||||
token: localStorage.getItem("access_token") || "",
|
||||
refreshToken: localStorage.getItem("refresh_token") || "",
|
||||
token: getAccessToken("user"),
|
||||
refreshToken: getRefreshToken("user"),
|
||||
userId: Number(localStorage.getItem("user_id") || 0),
|
||||
phone: localStorage.getItem("phone") || "",
|
||||
nickname: localStorage.getItem("nickname") || "",
|
||||
@@ -46,21 +47,28 @@ export const useSessionStore = defineStore("session", {
|
||||
this.refreshToken = "";
|
||||
this.userId = 0;
|
||||
this.phone = "";
|
||||
this.nickname = "";
|
||||
this.avatarUrl = "";
|
||||
this.realnameStatus = "unknown";
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("refresh_token");
|
||||
localStorage.removeItem("user_id");
|
||||
localStorage.removeItem("phone");
|
||||
localStorage.removeItem("nickname");
|
||||
localStorage.removeItem("avatar_url");
|
||||
localStorage.removeItem("realname_status");
|
||||
clearAuthStorage("user");
|
||||
},
|
||||
syncFromStorage() {
|
||||
this.token = getAccessToken("user");
|
||||
this.refreshToken = getRefreshToken("user");
|
||||
this.userId = Number(localStorage.getItem("user_id") || 0);
|
||||
this.phone = localStorage.getItem("phone") || "";
|
||||
this.nickname = localStorage.getItem("nickname") || "";
|
||||
this.avatarUrl = localStorage.getItem("avatar_url") || "";
|
||||
this.realnameStatus = localStorage.getItem("realname_status") || "unknown";
|
||||
},
|
||||
applySession(user: AuthUser, accessToken: string, refreshToken: string) {
|
||||
this.token = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
this.userId = user.id;
|
||||
localStorage.setItem("access_token", accessToken);
|
||||
localStorage.setItem("refresh_token", refreshToken);
|
||||
setAuthTokens("user", {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
localStorage.setItem("user_id", String(user.id));
|
||||
this.applyUser(user);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
export const listingStatuses = ['draft', 'published', 'rented', 'offline', 'abnormal'] as const
|
||||
export type ListingStatus = (typeof listingStatuses)[number]
|
||||
|
||||
export const listingReviewStatuses = ['none', 'pending', 'approved', 'rejected'] as const
|
||||
export type ListingReviewStatus = (typeof listingReviewStatuses)[number]
|
||||
|
||||
export const orderStatuses = [
|
||||
'pending_confirm',
|
||||
'pending_payment',
|
||||
'pending_handoff',
|
||||
'renting',
|
||||
'overdue',
|
||||
'pending_return_confirm',
|
||||
'pending_checkout_confirm',
|
||||
'pending_checkout_accept',
|
||||
'checkout_disputing',
|
||||
'completed',
|
||||
'cancelled',
|
||||
'closed',
|
||||
'disputing',
|
||||
'abnormal',
|
||||
] as const
|
||||
export type OrderStatus = (typeof orderStatuses)[number]
|
||||
|
||||
export const handoffStatuses = [
|
||||
'pending_owner',
|
||||
'pending_renter_confirm',
|
||||
'received',
|
||||
'pending_owner_return_confirm',
|
||||
'pending_owner_checkout',
|
||||
'pending_renter_checkout',
|
||||
'checkout_disputed',
|
||||
'returned',
|
||||
'cancelled',
|
||||
'owner_timeout',
|
||||
'renter_confirm_timeout',
|
||||
'return_overdue',
|
||||
'owner_return_confirm_timeout',
|
||||
'owner_checkout_confirm_timeout',
|
||||
'admin_closed',
|
||||
'admin_abnormal',
|
||||
'arbitrated',
|
||||
] as const
|
||||
export type HandoffStatus = (typeof handoffStatuses)[number]
|
||||
|
||||
export const settlementStatuses = [
|
||||
'unsettled',
|
||||
'pending',
|
||||
'frozen',
|
||||
'settled',
|
||||
'refunded',
|
||||
'cancelled',
|
||||
'closed',
|
||||
'disputed',
|
||||
'arbitrated',
|
||||
] as const
|
||||
export type SettlementStatus = (typeof settlementStatuses)[number]
|
||||
|
||||
export const realnameStatuses = ['unknown', 'unverified', 'pending', 'verified', 'rejected'] as const
|
||||
export type RealnameStatusValue = (typeof realnameStatuses)[number]
|
||||
|
||||
export const userStatuses = ['active', 'frozen', 'disabled'] as const
|
||||
export type UserStatus = (typeof userStatuses)[number]
|
||||
|
||||
export const riskStatuses = ['normal', 'watch', 'restricted', 'blocked'] as const
|
||||
export type RiskStatus = (typeof riskStatuses)[number]
|
||||
|
||||
export const disputeStatuses = ['open', 'processing', 'resolved', 'closed'] as const
|
||||
export type DisputeStatus = (typeof disputeStatuses)[number]
|
||||
|
||||
export const walletStatuses = ['active', 'frozen', 'disabled'] as const
|
||||
export type WalletStatus = (typeof walletStatuses)[number]
|
||||
|
||||
export const ledgerDirections = ['in', 'out', 'freeze', 'unfreeze'] as const
|
||||
export type LedgerDirection = (typeof ledgerDirections)[number]
|
||||
|
||||
export const balanceTypes = ['available', 'frozen'] as const
|
||||
export type BalanceType = (typeof balanceTypes)[number]
|
||||
@@ -0,0 +1,54 @@
|
||||
export type AuthScope = 'user' | 'admin'
|
||||
|
||||
export interface AuthTokenPair {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
}
|
||||
|
||||
const userKeys = {
|
||||
accessToken: 'access_token',
|
||||
refreshToken: 'refresh_token',
|
||||
profile: ['user_id', 'phone', 'nickname', 'avatar_url', 'realname_status'],
|
||||
}
|
||||
|
||||
const adminKeys = {
|
||||
accessToken: 'admin_access_token',
|
||||
refreshToken: 'admin_refresh_token',
|
||||
profile: ['admin_id', 'admin_username'],
|
||||
}
|
||||
|
||||
function keysFor(scope: AuthScope) {
|
||||
return scope === 'admin' ? adminKeys : userKeys
|
||||
}
|
||||
|
||||
export function getAccessToken(scope: AuthScope) {
|
||||
return localStorage.getItem(keysFor(scope).accessToken) || ''
|
||||
}
|
||||
|
||||
export function getRefreshToken(scope: AuthScope) {
|
||||
return localStorage.getItem(keysFor(scope).refreshToken) || ''
|
||||
}
|
||||
|
||||
export function setAuthTokens(scope: AuthScope, tokens: AuthTokenPair) {
|
||||
const keys = keysFor(scope)
|
||||
localStorage.setItem(keys.accessToken, tokens.access_token)
|
||||
localStorage.setItem(keys.refreshToken, tokens.refresh_token)
|
||||
notifyAuthStorageChanged(scope)
|
||||
}
|
||||
|
||||
export function clearAuthStorage(scope: AuthScope) {
|
||||
const keys = keysFor(scope)
|
||||
localStorage.removeItem(keys.accessToken)
|
||||
localStorage.removeItem(keys.refreshToken)
|
||||
keys.profile.forEach((key) => localStorage.removeItem(key))
|
||||
notifyAuthStorageChanged(scope)
|
||||
}
|
||||
|
||||
export function getLoginPath(scope: AuthScope, currentPath: string) {
|
||||
if (scope === 'admin') return '/admin/login'
|
||||
return currentPath.startsWith('/m') ? '/m/login' : '/login'
|
||||
}
|
||||
|
||||
export function notifyAuthStorageChanged(scope: AuthScope) {
|
||||
window.dispatchEvent(new CustomEvent('auth-storage-changed', { detail: { scope } }))
|
||||
}
|
||||
@@ -1,4 +1,19 @@
|
||||
const listingStatusMap: Record<string, string> = {
|
||||
import type {
|
||||
BalanceType,
|
||||
DisputeStatus,
|
||||
HandoffStatus,
|
||||
LedgerDirection,
|
||||
ListingReviewStatus,
|
||||
ListingStatus,
|
||||
OrderStatus,
|
||||
RealnameStatusValue,
|
||||
RiskStatus,
|
||||
SettlementStatus,
|
||||
UserStatus,
|
||||
WalletStatus,
|
||||
} from '@/types/status'
|
||||
|
||||
const listingStatusMap: Record<ListingStatus, string> = {
|
||||
draft: '草稿',
|
||||
published: '已上架',
|
||||
rented: '租用中',
|
||||
@@ -6,14 +21,14 @@ const listingStatusMap: Record<string, string> = {
|
||||
abnormal: '异常',
|
||||
}
|
||||
|
||||
const listingReviewStatusMap: Record<string, string> = {
|
||||
const listingReviewStatusMap: Record<ListingReviewStatus, string> = {
|
||||
none: '未提交',
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
}
|
||||
|
||||
const orderStatusMap: Record<string, string> = {
|
||||
const orderStatusMap: Record<OrderStatus, string> = {
|
||||
pending_confirm: '待确认',
|
||||
pending_payment: '待支付',
|
||||
pending_handoff: '待交接',
|
||||
@@ -30,7 +45,7 @@ const orderStatusMap: Record<string, string> = {
|
||||
abnormal: '异常',
|
||||
}
|
||||
|
||||
const handoffStatusMap: Record<string, string> = {
|
||||
const handoffStatusMap: Record<HandoffStatus, string> = {
|
||||
pending_owner: '待号主交接',
|
||||
pending_renter_confirm: '待租客确认',
|
||||
received: '已确认收号',
|
||||
@@ -50,7 +65,7 @@ const handoffStatusMap: Record<string, string> = {
|
||||
arbitrated: '已仲裁',
|
||||
}
|
||||
|
||||
const settlementStatusMap: Record<string, string> = {
|
||||
const settlementStatusMap: Record<SettlementStatus, string> = {
|
||||
unsettled: '未结算',
|
||||
pending: '待结算',
|
||||
frozen: '冻结中',
|
||||
@@ -62,95 +77,99 @@ const settlementStatusMap: Record<string, string> = {
|
||||
arbitrated: '已仲裁',
|
||||
}
|
||||
|
||||
const realnameStatusMap: Record<string, string> = {
|
||||
const realnameStatusMap: Partial<Record<RealnameStatusValue, string>> = {
|
||||
unverified: '未认证',
|
||||
pending: '认证中',
|
||||
verified: '已认证',
|
||||
rejected: '认证失败',
|
||||
}
|
||||
|
||||
const userStatusMap: Record<string, string> = {
|
||||
const userStatusMap: Record<UserStatus, string> = {
|
||||
active: '正常',
|
||||
frozen: '已冻结',
|
||||
disabled: '已禁用',
|
||||
}
|
||||
|
||||
const riskStatusMap: Record<string, string> = {
|
||||
const riskStatusMap: Record<RiskStatus, string> = {
|
||||
normal: '正常',
|
||||
watch: '观察',
|
||||
restricted: '受限',
|
||||
blocked: '已拦截',
|
||||
}
|
||||
|
||||
const disputeStatusMap: Record<string, string> = {
|
||||
const disputeStatusMap: Record<DisputeStatus, string> = {
|
||||
open: '待处理',
|
||||
processing: '处理中',
|
||||
resolved: '已处理',
|
||||
closed: '已关闭',
|
||||
}
|
||||
|
||||
const walletStatusMap: Record<string, string> = {
|
||||
const walletStatusMap: Record<WalletStatus, string> = {
|
||||
active: '正常',
|
||||
frozen: '已冻结',
|
||||
disabled: '已禁用',
|
||||
}
|
||||
|
||||
const ledgerDirectionMap: Record<string, string> = {
|
||||
const ledgerDirectionMap: Record<LedgerDirection, string> = {
|
||||
in: '收入',
|
||||
out: '支出',
|
||||
freeze: '冻结',
|
||||
unfreeze: '解冻',
|
||||
}
|
||||
|
||||
const balanceTypeMap: Record<string, string> = {
|
||||
const balanceTypeMap: Record<BalanceType, string> = {
|
||||
available: '可用余额',
|
||||
frozen: '冻结余额',
|
||||
}
|
||||
|
||||
function readLabel(map: Record<string, string>, value: string) {
|
||||
return map[value] || value || '-'
|
||||
}
|
||||
|
||||
export function listingStatusLabel(status: string) {
|
||||
return listingStatusMap[status] || status || '-'
|
||||
return readLabel(listingStatusMap, status)
|
||||
}
|
||||
|
||||
export function listingReviewStatusLabel(status: string) {
|
||||
return listingReviewStatusMap[status] || status || '-'
|
||||
return readLabel(listingReviewStatusMap, status)
|
||||
}
|
||||
|
||||
export function orderStatusLabel(status: string) {
|
||||
return orderStatusMap[status] || status || '-'
|
||||
return readLabel(orderStatusMap, status)
|
||||
}
|
||||
|
||||
export function handoffStatusLabel(status: string) {
|
||||
return handoffStatusMap[status] || status || '-'
|
||||
return readLabel(handoffStatusMap, status)
|
||||
}
|
||||
|
||||
export function settlementStatusLabel(status: string) {
|
||||
return settlementStatusMap[status] || status || '-'
|
||||
return readLabel(settlementStatusMap, status)
|
||||
}
|
||||
|
||||
export function realnameStatusLabel(status: string) {
|
||||
return realnameStatusMap[status] || status || '-'
|
||||
return readLabel(realnameStatusMap, status)
|
||||
}
|
||||
|
||||
export function userStatusLabel(status: string) {
|
||||
return userStatusMap[status] || status || '-'
|
||||
return readLabel(userStatusMap, status)
|
||||
}
|
||||
|
||||
export function riskStatusLabel(status: string) {
|
||||
return riskStatusMap[status] || status || '-'
|
||||
return readLabel(riskStatusMap, status)
|
||||
}
|
||||
|
||||
export function disputeStatusLabel(status: string) {
|
||||
return disputeStatusMap[status] || status || '-'
|
||||
return readLabel(disputeStatusMap, status)
|
||||
}
|
||||
|
||||
export function walletStatusLabel(status: string) {
|
||||
return walletStatusMap[status] || status || '-'
|
||||
return readLabel(walletStatusMap, status)
|
||||
}
|
||||
|
||||
export function ledgerDirectionLabel(direction: string) {
|
||||
return ledgerDirectionMap[direction] || direction || '-'
|
||||
return readLabel(ledgerDirectionMap, direction)
|
||||
}
|
||||
|
||||
export function balanceTypeLabel(type: string) {
|
||||
return balanceTypeMap[type] || type || '-'
|
||||
return readLabel(balanceTypeMap, type)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { fetchAdminListings, type Listing } from '@/api/listings'
|
||||
import { fetchAdminListings, type AdminListingQuery, type Listing } from '@/api/listings'
|
||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
const listings = ref<Listing[]>([])
|
||||
const filters = reactive({
|
||||
const filters = reactive<AdminListingQuery>({
|
||||
owner_id: '',
|
||||
status: '',
|
||||
review_status: '',
|
||||
|
||||
Reference in New Issue
Block a user