feat;提交管理端和城市合伙人端
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
function resolveApiBase(): string {
|
||||
const origin =
|
||||
typeof TARO_APP_API_ORIGIN !== 'undefined' && TARO_APP_API_ORIGIN
|
||||
? TARO_APP_API_ORIGIN
|
||||
: process.env.TARO_ENV === 'h5'
|
||||
? 'http://localhost:3000'
|
||||
: '';
|
||||
if (origin) {
|
||||
return `${origin.replace(/\/$/, '')}/api/v1`;
|
||||
}
|
||||
return '/api/v1';
|
||||
}
|
||||
|
||||
export const API_BASE = resolveApiBase();
|
||||
const TOKEN_KEY = 'hq_access_token';
|
||||
const CLIENT_APP = 'HQ_WEB';
|
||||
|
||||
export function getToken(): string {
|
||||
try {
|
||||
return Taro.getStorageSync(TOKEN_KEY) || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function saveToken(token: string) {
|
||||
Taro.setStorageSync(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function clearToken() {
|
||||
Taro.removeStorageSync(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function isLoggedIn(): boolean {
|
||||
return !!getToken();
|
||||
}
|
||||
|
||||
export function redirectToLogin() {
|
||||
Taro.reLaunch({ url: '/pages/login/index' });
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
clearToken();
|
||||
redirectToLogin();
|
||||
}
|
||||
|
||||
type ReqOptions = {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
data?: Record<string, unknown> | unknown;
|
||||
auth?: boolean;
|
||||
};
|
||||
|
||||
function parseBody(data: unknown): { code?: number; message?: string } {
|
||||
if (data && typeof data === 'object') {
|
||||
return data as { code?: number; message?: string };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/** 统一请求:注入 X-Client-App + Bearer,解包 { code, message, data },401 自动回登录 */
|
||||
export async function request<T = unknown>(path: string, options: ReqOptions = {}): Promise<T> {
|
||||
const header: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-App': CLIENT_APP,
|
||||
};
|
||||
const token = getToken();
|
||||
if (token) header.Authorization = `Bearer ${token}`;
|
||||
|
||||
const res = await Taro.request({
|
||||
url: `${API_BASE}${path}`,
|
||||
method: options.method ?? 'GET',
|
||||
data: options.data as Record<string, unknown>,
|
||||
header,
|
||||
});
|
||||
|
||||
const status = res.statusCode;
|
||||
const body = parseBody(res.data);
|
||||
|
||||
if (status === 401 || body?.code === 401) {
|
||||
clearToken();
|
||||
redirectToLogin();
|
||||
throw new Error(body?.message || '登录已过期,请重新登录');
|
||||
}
|
||||
if (status === 404 && body.code === undefined) {
|
||||
throw new Error('接口不可达,请确认 API 服务已启动');
|
||||
}
|
||||
if (status >= 400 || body.code !== 0) {
|
||||
throw new Error(body?.message || `请求失败(${status})`);
|
||||
}
|
||||
return (res.data as { data: T }).data;
|
||||
}
|
||||
|
||||
export type Paginated<T> = { items: T[]; total: number };
|
||||
|
||||
export function toast(title: string, icon: 'success' | 'error' | 'none' = 'none') {
|
||||
Taro.showToast({ title, icon, duration: 1800 });
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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 CITY_STATUS_LABELS: Record<string, string> = {
|
||||
PENDING: '待开城',
|
||||
ACTIVE: '已开城',
|
||||
PAUSED: '已暂停',
|
||||
};
|
||||
|
||||
export const PRODUCT_STATUS_LABELS: Record<string, string> = {
|
||||
DRAFT: '草稿',
|
||||
ON_SALE: '在售',
|
||||
OFF_SALE: '下架',
|
||||
};
|
||||
|
||||
export function badgeClass(status: string): string {
|
||||
if (['OPEN', 'ACTIVE', 'ON_SALE', 'COMPLETED', 'PAID', 'CONFIRMED'].includes(status)) return 'hq-badge--ok';
|
||||
if (['PENDING', 'PENDING_PAY', 'PENDING_SHIP', 'DRAFT', 'PENDING_RECEIVE'].includes(status)) return 'hq-badge--warn';
|
||||
if (['CLOSED', 'CANCELLED', 'REFUNDED', 'OFF_SALE', 'VOID'].includes(status)) return 'hq-badge--danger';
|
||||
return 'hq-badge--info';
|
||||
}
|
||||
|
||||
export function fmtTime(v?: string | null): string {
|
||||
return v ? new Date(v).toLocaleString('zh-CN') : '—';
|
||||
}
|
||||
|
||||
export function fmtMoney(v?: number | string | null): string {
|
||||
const n = Number(v ?? 0);
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { isLoggedIn, redirectToLogin, request } from './api';
|
||||
|
||||
export type HqAccount = {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
adminRole: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
let cache: HqAccount | null = null;
|
||||
|
||||
export async function fetchHqAccount(force = false): Promise<HqAccount | null> {
|
||||
if (cache && !force) return cache;
|
||||
if (!isLoggedIn()) return null;
|
||||
try {
|
||||
cache = await request<HqAccount>('/admin/auth/me');
|
||||
return cache;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearHqAccountCache() {
|
||||
cache = null;
|
||||
}
|
||||
|
||||
/** 页面级会话守卫:未登录跳登录页;返回当前 HQ 账号 */
|
||||
export function useHqSession(guard = true) {
|
||||
const [account, setAccount] = useState<HqAccount | null>(cache);
|
||||
const [loading, setLoading] = useState(!cache);
|
||||
|
||||
useEffect(() => {
|
||||
if (guard && !isLoggedIn()) {
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
let alive = true;
|
||||
void fetchHqAccount().then((acc) => {
|
||||
if (!alive) return;
|
||||
setAccount(acc);
|
||||
setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return { account, loading };
|
||||
}
|
||||
|
||||
export function roleLabel(role?: string): string {
|
||||
switch (role) {
|
||||
case 'SUPER_ADMIN':
|
||||
return '超级管理员';
|
||||
case 'OPS':
|
||||
return '运营';
|
||||
case 'FINANCE':
|
||||
return '财务';
|
||||
case 'SUPPORT':
|
||||
return '客服';
|
||||
default:
|
||||
return role || '管理员';
|
||||
}
|
||||
}
|
||||
|
||||
export function navTo(url: string) {
|
||||
Taro.navigateTo({ url });
|
||||
}
|
||||
|
||||
export function switchToTab(url: string) {
|
||||
Taro.switchTab({ url });
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { request, saveToken } from './api';
|
||||
import { isWechatEnv, weixinSdk } from './weixin';
|
||||
|
||||
let clientConfigCache: { mockWechat?: boolean } | null = null;
|
||||
|
||||
async function fetchClientConfig(): Promise<{ mockWechat?: boolean }> {
|
||||
if (clientConfigCache) return clientConfigCache;
|
||||
try {
|
||||
clientConfigCache = await request<{ mockWechat?: boolean }>('/common/client-config');
|
||||
} catch {
|
||||
clientConfigCache = {};
|
||||
}
|
||||
return clientConfigCache;
|
||||
}
|
||||
|
||||
/** 处理微信登录/绑定结果,返回是否已拿到 token 可进入首页 */
|
||||
export function handleHqWechatLoginResult(result: WechatLoginResult): boolean {
|
||||
if (!result.accessToken) return false;
|
||||
saveToken(result.accessToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 微信内 OAuth 回调:URL 带 code 时兑换 token */
|
||||
export async function handleHqWechatCallback(): Promise<WechatLoginResult | null> {
|
||||
if (process.env.TARO_ENV === 'weapp') {
|
||||
return null;
|
||||
}
|
||||
if (!isWechatEnv()) return null;
|
||||
return weixinSdk.handleOAuthCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信一键登录(与合伙人端一致:微信内走公众号 OAuth,浏览器 Mock 演示)。
|
||||
* 返回 true = 已登录;void = 已跳转授权页等待回调。
|
||||
*/
|
||||
export async function loginHqWithWechat(): Promise<boolean | void> {
|
||||
if (process.env.TARO_ENV === 'weapp') {
|
||||
const res = await Taro.login();
|
||||
const result = await request<WechatLoginResult>('/admin/auth/login/wechat', {
|
||||
method: 'POST',
|
||||
data: { code: res.code, platform: 'mini' },
|
||||
});
|
||||
return handleHqWechatLoginResult(result);
|
||||
}
|
||||
|
||||
if (isWechatEnv()) {
|
||||
const result = await weixinSdk.login();
|
||||
if (result) return handleHqWechatLoginResult(result);
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = await fetchClientConfig();
|
||||
if (!cfg.mockWechat) {
|
||||
throw new Error('请在微信内打开以使用微信一键登录');
|
||||
}
|
||||
const result = await request<WechatLoginResult>('/admin/auth/login/wechat', {
|
||||
method: 'POST',
|
||||
data: { code: `mockcode_${Date.now()}`, platform: 'h5' },
|
||||
});
|
||||
return handleHqWechatLoginResult(result);
|
||||
}
|
||||
|
||||
/** 短信登录成功后于微信内自动发起 OAuth,绑定 openId 便于后续免登 */
|
||||
export async function bindHqWechatAfterSmsLogin(): Promise<void> {
|
||||
if (process.env.TARO_ENV === 'weapp') {
|
||||
const res = await Taro.login();
|
||||
const result = await request<WechatLoginResult>('/admin/auth/login/wechat', {
|
||||
method: 'POST',
|
||||
data: { code: res.code, platform: 'mini' },
|
||||
});
|
||||
handleHqWechatLoginResult(result);
|
||||
return;
|
||||
}
|
||||
if (!isWechatEnv()) return;
|
||||
await weixinSdk.login();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createWeixinSdk, isWechatEnv } from '@dukang/weixin-sdk';
|
||||
import { API_BASE, getToken } from './api';
|
||||
|
||||
export const weixinSdk = createWeixinSdk({
|
||||
apiBase: API_BASE,
|
||||
clientApp: 'HQ_WEB',
|
||||
getAccessToken: () => getToken(),
|
||||
wechatLoginPath: '/admin/auth/login/wechat',
|
||||
});
|
||||
|
||||
export { isWechatEnv };
|
||||
Reference in New Issue
Block a user