forked from wangziqi/gongxue-base
feat: scaffold taro h5 frontends
This commit is contained in:
132
apps/taro/src/services/api.ts
Normal file
132
apps/taro/src/services/api.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
import { appEnv } from '@/env';
|
||||
import type { ApiErrorPayload, ApiSession, TenantContext } from '@/types';
|
||||
import { getStorage, removeStorage, setStorage } from './storage';
|
||||
|
||||
const TENANT_KEY = 'tiku:tenant';
|
||||
const SESSION_KEY = 'tiku:session';
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
code: string;
|
||||
details?: unknown;
|
||||
|
||||
constructor(payload: ApiErrorPayload) {
|
||||
super(payload.message);
|
||||
this.name = 'ApiError';
|
||||
this.status = payload.status;
|
||||
this.code = payload.code;
|
||||
this.details = payload.details;
|
||||
}
|
||||
}
|
||||
|
||||
export function getTenantContext() {
|
||||
return getStorage<TenantContext>(TENANT_KEY);
|
||||
}
|
||||
|
||||
export function saveTenantContext(tenant: TenantContext) {
|
||||
setStorage(TENANT_KEY, tenant);
|
||||
}
|
||||
|
||||
export function clearTenantContext() {
|
||||
removeStorage(TENANT_KEY);
|
||||
}
|
||||
|
||||
export function getSession() {
|
||||
return getStorage<ApiSession>(SESSION_KEY);
|
||||
}
|
||||
|
||||
export function saveSession(session: ApiSession) {
|
||||
setStorage(SESSION_KEY, session);
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
removeStorage(SESSION_KEY);
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string) {
|
||||
return baseUrl.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function buildUrl(path: string, query?: Record<string, string | number | boolean | null | undefined>) {
|
||||
const url = `${normalizeBaseUrl(appEnv.apiBaseUrl)}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
const params = Object.entries(query || {})
|
||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
return params.length ? `${url}?${params.join('&')}` : url;
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
options: {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
query?: Record<string, string | number | boolean | null | undefined>;
|
||||
body?: unknown;
|
||||
tenantId?: string | null;
|
||||
token?: string | null;
|
||||
headers?: Record<string, string>;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
const tenant = getTenantContext();
|
||||
const session = getSession();
|
||||
const token = options.token ?? session?.token ?? null;
|
||||
const tenantId = options.tenantId ?? tenant?.tenantId ?? null;
|
||||
const headers: Record<string, string> = {
|
||||
'content-type': 'application/json',
|
||||
...(tenantId ? { 'x-tenant-id': tenantId } : {}),
|
||||
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const response = await Taro.request({
|
||||
url: buildUrl(path, options.query),
|
||||
method: options.method || 'GET',
|
||||
data: options.body === undefined ? undefined : options.body,
|
||||
header: headers,
|
||||
});
|
||||
const payload = (response.data || {}) as Record<string, unknown>;
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
if (response.statusCode === 401) clearSession();
|
||||
throw new ApiError({
|
||||
status: response.statusCode,
|
||||
code: String(payload.code || 'API_ERROR'),
|
||||
message: String(payload.message || '请求失败'),
|
||||
details: payload,
|
||||
});
|
||||
}
|
||||
return payload as T;
|
||||
}
|
||||
|
||||
export async function resolveTenant(input: { host?: string; tenantCode?: string } = {}) {
|
||||
const payload = await apiRequest<{
|
||||
item?: TenantContext;
|
||||
tenant?: {
|
||||
id?: string;
|
||||
tenantId?: string;
|
||||
slug?: string;
|
||||
};
|
||||
branding?: TenantContext['branding'];
|
||||
features?: TenantContext['features'];
|
||||
adminFeatures?: TenantContext['adminFeatures'];
|
||||
publicConfig?: TenantContext['publicConfig'];
|
||||
}>('/api/tenant/resolve', {
|
||||
query: {
|
||||
host: input.host,
|
||||
tenantCode: input.tenantCode || appEnv.tenantCode,
|
||||
},
|
||||
tenantId: null,
|
||||
});
|
||||
const tenantId = payload.item?.tenantId || payload.tenant?.tenantId || payload.tenant?.id;
|
||||
if (!tenantId) throw new ApiError({ status: 500, code: 'TENANT_RESOLVE_INVALID', message: '租户解析结果缺少 tenantId' });
|
||||
const context: TenantContext = {
|
||||
tenantId,
|
||||
tenantSlug: payload.item?.tenantSlug || payload.tenant?.slug,
|
||||
host: input.host,
|
||||
branding: payload.item?.branding || payload.branding || {},
|
||||
features: payload.item?.features || payload.features || {},
|
||||
adminFeatures: payload.item?.adminFeatures || payload.adminFeatures || {},
|
||||
publicConfig: payload.item?.publicConfig || payload.publicConfig || {},
|
||||
};
|
||||
saveTenantContext(context);
|
||||
return context;
|
||||
}
|
||||
31
apps/taro/src/services/auth.ts
Normal file
31
apps/taro/src/services/auth.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { apiRequest, clearSession, saveSession } from './api';
|
||||
import type { ApiEnvelope, CurrentUser } from '@/types';
|
||||
|
||||
export async function sendSmsCode(phone: string, purpose: 'login' | 'bind_phone' = 'login') {
|
||||
return apiRequest<ApiEnvelope<never>>('/api/auth/sms/send', {
|
||||
method: 'POST',
|
||||
body: { phone, purpose },
|
||||
tenantId: null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifySmsCode(phone: string, code: string, purpose: 'login' | 'bind_phone' = 'login') {
|
||||
const payload = await apiRequest<ApiEnvelope<CurrentUser>>('/api/auth/sms/verify', {
|
||||
method: 'POST',
|
||||
body: { phone, code, purpose },
|
||||
});
|
||||
if (payload.session?.token) saveSession(payload.session);
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function loadCurrentUser() {
|
||||
return apiRequest<ApiEnvelope<CurrentUser>>('/api/auth/me');
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
try {
|
||||
await apiRequest('/api/auth/logout', { method: 'POST' });
|
||||
} finally {
|
||||
clearSession();
|
||||
}
|
||||
}
|
||||
32
apps/taro/src/services/catalog.ts
Normal file
32
apps/taro/src/services/catalog.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { apiRequest } from './api';
|
||||
|
||||
export interface ContentEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
entryType?: string;
|
||||
route?: string;
|
||||
icon?: string;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface DashboardSnapshot {
|
||||
entries: ContentEntry[];
|
||||
banners: unknown[];
|
||||
announcements: unknown[];
|
||||
profile: unknown | null;
|
||||
}
|
||||
|
||||
export async function loadStudentDashboard(regionId?: string): Promise<DashboardSnapshot> {
|
||||
const [entries, banners, announcements, profile] = await Promise.all([
|
||||
apiRequest<{ items?: ContentEntry[] }>('/api/catalog/content-entries', { query: { regionId } }),
|
||||
apiRequest<{ items?: unknown[] }>('/api/catalog/banners'),
|
||||
apiRequest<{ items?: unknown[] }>('/api/catalog/announcements'),
|
||||
apiRequest<{ item?: unknown }>('/api/profile/me').catch(() => ({ item: null })),
|
||||
]);
|
||||
return {
|
||||
entries: entries.items || [],
|
||||
banners: banners.items || [],
|
||||
announcements: announcements.items || [],
|
||||
profile: profile.item || null,
|
||||
};
|
||||
}
|
||||
19
apps/taro/src/services/storage.ts
Normal file
19
apps/taro/src/services/storage.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Taro from '@tarojs/taro';
|
||||
|
||||
export function getStorage<T>(key: string): T | null {
|
||||
try {
|
||||
const value = Taro.getStorageSync<string>(key);
|
||||
if (!value) return null;
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setStorage<T>(key: string, value: T) {
|
||||
Taro.setStorageSync(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
export function removeStorage(key: string) {
|
||||
Taro.removeStorageSync(key);
|
||||
}
|
||||
25
apps/taro/src/services/supabase.ts
Normal file
25
apps/taro/src/services/supabase.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
||||
import { appEnv } from '@/env';
|
||||
|
||||
let client: SupabaseClient | null = null;
|
||||
|
||||
export function getSupabaseClient() {
|
||||
if (!appEnv.supabaseUrl || !appEnv.supabasePublishableKey) return null;
|
||||
if (!client) {
|
||||
client = createClient(appEnv.supabaseUrl, appEnv.supabasePublishableKey, {
|
||||
auth: {
|
||||
persistSession: true,
|
||||
autoRefreshToken: true,
|
||||
detectSessionInUrl: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function getSupabaseAccessToken() {
|
||||
const supabase = getSupabaseClient();
|
||||
if (!supabase) return null;
|
||||
const { data } = await supabase.auth.getSession();
|
||||
return data.session?.access_token || null;
|
||||
}
|
||||
Reference in New Issue
Block a user