feat(miniprogram): 初始化 Taro 4 + React 小程序项目
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 手动创建 Taro 4.2 + React 18 + TypeScript 项目骨架
- 配置 webpack5 编译、SCSS 样式、医疗清新主题
- 实现 API 请求层(JWT 自动注入 + token 刷新)
- 实现 auth store(微信登录 + 手机号绑定 + 就诊人管理)
- 实现登录页(微信一键登录 + 手机号授权绑定)
- 实现首页(问候栏 + 今日健康卡片 + 快捷服务 + 即将到来)
- 实现我的页面(个人信息 + 功能菜单 + 退出登录)
- 健康/预约/资讯占位页
- TabBar 5 个入口:首页/健康/预约/资讯/我的
This commit is contained in:
iven
2026-04-24 00:28:38 +08:00
parent 47817bae7d
commit 0f84c881ef
30 changed files with 17555 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import { api } from './request';
export interface UserInfo {
id: string;
name: string;
phone: string;
avatar?: string;
tenant_id: string;
}
export interface LoginResp {
bound: boolean;
openid: string;
token?: {
access_token: string;
refresh_token: string;
expires_in: number;
user: { id: string; username: string; display_name?: string; phone?: string; avatar_url?: string };
};
}
export interface PatientInfo {
id: string;
name: string;
gender?: string;
birthday?: string;
relation: string;
}
export async function wechatLogin(code: string): Promise<LoginResp> {
return api.post('/auth/wechat/login', { code });
}
export async function wechatBindPhone(openid: string, encryptedData: string, iv: string) {
return api.post('/auth/wechat/bind-phone', {
openid,
encrypted_data: encryptedData,
iv,
});
}
export async function getPatients() {
return api.get<PatientInfo[]>('/health/patients');
}

View File

@@ -0,0 +1,65 @@
import Taro from '@tarojs/taro';
const BASE_URL = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1';
interface ApiResponse<T> {
success: boolean;
data?: T;
message?: string;
}
async function getHeaders(): Promise<Record<string, string>> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
const token = Taro.getStorageSync('access_token');
if (token) headers['Authorization'] = `Bearer ${token}`;
const patientId = Taro.getStorageSync('current_patient_id');
if (patientId) headers['X-Patient-Id'] = patientId;
const tenantId = Taro.getStorageSync('tenant_id');
if (tenantId) headers['X-Tenant-Id'] = tenantId;
return headers;
}
async function tryRefreshToken(): Promise<boolean> {
const refreshToken = Taro.getStorageSync('refresh_token');
if (!refreshToken) return false;
try {
const res = await Taro.request({
url: `${BASE_URL}/auth/refresh`,
method: 'POST',
data: { refresh_token: refreshToken },
});
if (res.statusCode === 200 && res.data?.success) {
Taro.setStorageSync('access_token', res.data.data.access_token);
Taro.setStorageSync('refresh_token', res.data.data.refresh_token);
return true;
}
} catch {
// ignore
}
Taro.removeStorageSync('access_token');
Taro.removeStorageSync('refresh_token');
return false;
}
export async function request<T>(method: string, path: string, data?: unknown): Promise<T> {
const headers = await getHeaders();
const res = await Taro.request({ url: `${BASE_URL}${path}`, method, data, header: headers });
if (res.statusCode === 401) {
const refreshed = await tryRefreshToken();
if (refreshed) return request<T>(method, path, data);
Taro.redirectTo({ url: '/pages/login/index' });
throw new Error('登录已过期');
}
const body = res.data as ApiResponse<T>;
if (!body.success) throw new Error(body.message || '请求失败');
return body.data as T;
}
export const api = {
get: <T>(path: string) => request<T>('GET', path),
post: <T>(path: string, data?: unknown) => request<T>('POST', path, data),
put: <T>(path: string, data?: unknown) => request<T>('PUT', path, data),
delete: <T>(path: string) => request<T>('DELETE', path),
};