This commit is contained in:
2026-07-01 08:27:26 +08:00
parent 9f4577d3d8
commit 25f0d8e97b
56 changed files with 5298 additions and 2 deletions
+90
View File
@@ -0,0 +1,90 @@
export const apiBase = '/api/v1';
export const CLIENT_APP = 'HQ_WEB';
export type HqProfile = {
id: string;
phone: string;
name: string;
adminRole: string;
status: string;
};
export function getToken() {
return localStorage.getItem('accessToken');
}
export function saveAuth(data: { accessToken: string; refreshToken?: string }) {
localStorage.setItem('accessToken', data.accessToken);
if (data.refreshToken) localStorage.setItem('refreshToken', data.refreshToken);
}
export function clearAuth() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
export async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Client-App': CLIENT_APP,
...(options.headers as Record<string, string>),
};
const token = getToken();
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
const json = await res.json();
if (json.code === 401) {
clearAuth();
window.location.href = '/login';
throw new Error('未登录');
}
if (json.code !== 0) throw new Error(json.message || '请求失败');
return json.data as T;
}
export type Paginated<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
};
export type DashboardStats = {
usersTotal: number;
guestUsers: number;
verifiedUsers: number;
mergedUsers: number;
ordersToday: number;
storesTotal: number;
partnersTotal: number;
redeemToday: number;
deliveriesTotal: number;
ordersByStatus: Array<{ status: string; count: number }>;
};
export type AdminUserRow = {
id: string;
userNo: string;
deviceKey: string | null;
phone: string | null;
phoneVerifiedAt: string | null;
mergedIntoUserId: string | null;
nickname: string | null;
status: number;
createdAt: string;
orderCount: number;
};
export type AdminOrderRow = {
id: string;
orderNo: string;
status: string;
deliveryType: string;
payAmount: number;
receiverName: string;
receiverPhone: string;
createdAt: string;
user?: { id: string; userNo: string; phone: string | null; nickname: string | null };
delivery?: { provider: string; trackingNo: string | null; providerOrderNo: string | null };
};
+56
View File
@@ -0,0 +1,56 @@
export const ORDER_STATUS_LABELS: Record<string, string> = {
PENDING_PAY: '待付款',
PENDING_SHIP: '待发货',
OUT_WAREHOUSE: '已出库',
SHIPPING: '配送中',
PENDING_RECEIVE: '待收货',
COMPLETED: '已完成',
CANCELLED: '已取消',
REFUNDING: '退款中',
REFUNDED: '已退款',
};
export const STORE_STATUS_LABELS: Record<string, string> = {
OPEN: '营业中',
PAUSED: '暂停',
CLOSED: '已关闭',
};
export const ACCOUNT_STATUS_LABELS: Record<string, string> = {
ACTIVE: '正常',
DISABLED: '停用',
};
export const COUPON_STATUS_LABELS: Record<string, string> = {
ACTIVE: '可用',
USED_UP: '已用完',
VOID: '已作废',
};
export const CITY_STATUS_LABELS: Record<string, string> = {
PENDING: '待开城',
ACTIVE: '已开城',
PAUSED: '已暂停',
};
export const PARTNER_BILL_STATUS_LABELS: Record<string, string> = {
DRAFT: '草稿',
CONFIRMED: '已确认',
PAID: '已结算',
};
export const MEDIA_TYPE_LABELS: Record<string, string> = {
IMAGE: '图片',
VIDEO: '视频',
};
export const LEDGER_TYPE_LABELS: Record<string, string> = {
GRANT: '发放',
REDEEM: '核销',
REFUND_VOID: '退款作废',
ADJUST: '调整',
};
export function fmtTime(v?: string | null) {
return v ? new Date(v).toLocaleString('zh-CN') : '—';
}
+28
View File
@@ -0,0 +1,28 @@
import { useCallback, useEffect, useState } from 'react';
import { request, type Paginated } from './api';
export function useAdminList<T>(path: string, buildQuery: () => URLSearchParams, deps: unknown[]) {
const [data, setData] = useState<Paginated<T> | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const load = useCallback(async () => {
setLoading(true);
try {
const qs = buildQuery();
qs.set('page', String(page));
qs.set('pageSize', String(pageSize));
const res = await request<Paginated<T>>(`${path}?${qs}`);
setData(res);
} finally {
setLoading(false);
}
}, [path, page, pageSize, ...deps]);
useEffect(() => {
void load();
}, [load]);
return { data, loading, page, pageSize, setPage, setPageSize, reload: load };
}