refactor(miniprogram): 消灭全部 any 类型 — 32处 → 0 (E3-1)

- catch (err: any) → catch (err: unknown) + instanceof Error 类型缩窄(6 文件 13 处)
- BLE 回调类型提取 BLEScanResult/BLEConnectionChangeResult/BLECharacteristicChangeResult/BLEServiceItem
- BLEManager 7 处 any 注解替换为具体接口类型
- request.ts method as any → method as ValidMethod 字面量联合类型
- appointment ScheduleItem 接口定义替代 any[]
- Taro.requestSubscribeMessage 使用类型断言替代 as any
- globalThis.__hms 使用 Record<string, unknown>
- TrendChart Canvas/useRef 保留 eslint-disable(微信 API 限制)
- 0 TS 错误,119 测试通过
This commit is contained in:
iven
2026-05-22 08:30:01 +08:00
parent bdc2d07c1c
commit c9fe654d44
14 changed files with 99 additions and 50 deletions

View File

@@ -22,12 +22,12 @@ function App({ children }: PropsWithChildren<Record<string, unknown>>) {
// 暴露全局 bridge 供 MCP/自动化测试调用(仅 dev 模式) // 暴露全局 bridge 供 MCP/自动化测试调用(仅 dev 模式)
useEffect(() => { useEffect(() => {
if (process.env.NODE_ENV === 'production') return; if (process.env.NODE_ENV === 'production') return;
(globalThis as any).__hms = { (globalThis as Record<string, unknown>).__hms = {
restoreAuth: () => { restoreAuth(); return useAuthStore.getState(); }, restoreAuth: () => { restoreAuth(); return useAuthStore.getState(); },
restoreUI, restoreUI,
getAuthState: () => useAuthStore.getState(), getAuthState: () => useAuthStore.getState(),
}; };
return () => { delete (globalThis as any).__hms; }; return () => { delete (globalThis as Record<string, unknown>).__hms; };
}, []); }, []);
// Analytics 定时器:仅在页面可见时运行,后台时暂停以节省资源 // Analytics 定时器:仅在页面可见时运行,后台时暂停以节省资源

View File

@@ -46,6 +46,7 @@ export default React.memo(function TrendChart({
height = 500, height = 500,
}: TrendChartProps) { }: TrendChartProps) {
const tokens = useCanvasTokens(); const tokens = useCanvasTokens();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const canvasRef = useRef<any>(null); const canvasRef = useRef<any>(null);
const [tooltip, setTooltip] = useState<{ date: string; value: number; x: number } | null>(null); const [tooltip, setTooltip] = useState<{ date: string; value: number; x: number } | null>(null);
@@ -193,7 +194,7 @@ export default React.memo(function TrendChart({
ctx.restore(); ctx.restore();
}, [data, referenceMin, referenceMax, tokens]); }, [data, referenceMin, referenceMax, tokens]);
const handleTouchStart = useCallback((e: any) => { const handleTouchStart = useCallback((e: { touches: Array<{ x: number }> }) => {
if (!data || data.length === 0 || !canvasRef.current) return; if (!data || data.length === 0 || !canvasRef.current) return;
const touch = e.touches[0]; const touch = e.touches[0];
const node = canvasRef.current; const node = canvasRef.current;

View File

@@ -37,6 +37,16 @@ interface TimeSlot {
available_count: number; available_count: number;
} }
interface ScheduleItem {
date?: string;
appointment_date?: string;
start_time?: string;
end_time?: string;
available_count?: number;
max_appointments?: number;
current_appointments?: number;
}
export default function AppointmentCreate() { export default function AppointmentCreate() {
const [currentStep, setCurrentStep] = useState(0); const [currentStep, setCurrentStep] = useState(0);
const [department, setDepartment] = useState(''); const [department, setDepartment] = useState('');
@@ -47,7 +57,7 @@ export default function AppointmentCreate() {
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { safeSetTimeout } = useSafeTimeout(); const { safeSetTimeout } = useSafeTimeout();
const [schedules, setSchedules] = useState<any[]>([]); const [schedules, setSchedules] = useState<ScheduleItem[]>([]);
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]); const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
const modeClass = useElderClass(); const modeClass = useElderClass();
@@ -55,7 +65,7 @@ export default function AppointmentCreate() {
const scheduledDates = useMemo(() => { const scheduledDates = useMemo(() => {
if (!schedules) return new Set<string>(); if (!schedules) return new Set<string>();
return new Set(schedules.map((s: any) => s.date || s.appointment_date)); return new Set(schedules.map((s) => s.date || s.appointment_date || ''));
}, [schedules]); }, [schedules]);
const onSelectDept = useCallback(async (dept: string) => { const onSelectDept = useCallback(async (dept: string) => {
@@ -91,12 +101,12 @@ export default function AppointmentCreate() {
setAppointmentDate(date); setAppointmentDate(date);
setTimeSlot(''); setTimeSlot('');
const daySlots = schedules const daySlots = schedules
.filter((s: any) => (s.date || s.appointment_date) === date) .filter((s) => (s.date || s.appointment_date) === date)
.map((s: any) => ({ .map((s) => ({
start_time: s.start_time || '', start_time: s.start_time || '',
end_time: s.end_time || '', end_time: s.end_time || '',
label: `${s.start_time || ''}-${s.end_time || ''}`, label: `${s.start_time || ''}-${s.end_time || ''}`,
available_count: s.available_count ?? (s.max_appointments - (s.current_appointments || 0)), available_count: s.available_count ?? (s.max_appointments ?? 0) - (s.current_appointments ?? 0),
})); }));
setTimeSlots(daySlots); setTimeSlots(daySlots);
}, [schedules]); }, [schedules]);
@@ -129,12 +139,12 @@ export default function AppointmentCreate() {
const tmplId = TEMPLATE_IDS.APPOINTMENT_REMINDER; const tmplId = TEMPLATE_IDS.APPOINTMENT_REMINDER;
if (tmplId) { if (tmplId) {
try { try {
await (Taro.requestSubscribeMessage as any)({ tmplIds: [tmplId] }); await (Taro as { requestSubscribeMessage: (opts: { tmplIds: string[] }) => Promise<unknown> }).requestSubscribeMessage({ tmplIds: [tmplId] });
} catch { /* 用户拒绝 */ } } catch { /* 用户拒绝 */ }
} }
safeSetTimeout(() => Taro.navigateBack(), 1500); safeSetTimeout(() => Taro.navigateBack(), 1500);
} catch (err: any) { } catch (err: unknown) {
const msg = err?.message || '预约失败'; const msg = err instanceof Error ? err.message : '预约失败';
Taro.showToast({ title: msg.length > 20 ? msg.slice(0, 20) : msg, icon: 'none' }); Taro.showToast({ title: msg.length > 20 ? msg.slice(0, 20) : msg, icon: 'none' });
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -34,7 +34,7 @@ export default function ConsultationCreate() {
if (doctorsLoaded) return; if (doctorsLoaded) return;
try { try {
const res = await listDoctors(); const res = await listDoctors();
const items = (res.data || []).map((d: any) => ({ id: d.id, name: d.name })); const items = (res.data || []).map((d: { id: string; name: string }) => ({ id: d.id, name: d.name }));
setDoctorList(items); setDoctorList(items);
setDoctorsLoaded(true); setDoctorsLoaded(true);
} catch (err) { } catch (err) {

View File

@@ -86,8 +86,8 @@ export default function Login() {
setNeedBind(true); setNeedBind(true);
Taro.showToast({ title: '请授权手机号完成绑定', icon: 'none' }); Taro.showToast({ title: '请授权手机号完成绑定', icon: 'none' });
} }
} catch (err: any) { } catch (err: unknown) {
const msg = err?.message || '登录失败,请重试'; const msg = err instanceof Error ? err.message : '登录失败,请重试';
Taro.showToast({ title: msg.substring(0, 20), icon: 'none', duration: 3000 }); Taro.showToast({ title: msg.substring(0, 20), icon: 'none', duration: 3000 });
} }
}; };
@@ -104,8 +104,8 @@ export default function Login() {
if (success) { if (success) {
navigateAfterLogin(); navigateAfterLogin();
} }
} catch (err: any) { } catch (err: unknown) {
Taro.showToast({ title: err?.message || '登录失败', icon: 'none' }); Taro.showToast({ title: err instanceof Error ? err.message : '登录失败', icon: 'none' });
} }
}; };
@@ -120,10 +120,10 @@ export default function Login() {
if (success) { if (success) {
navigateAfterLogin(); navigateAfterLogin();
} }
} catch (err: any) { } catch (err: unknown) {
Taro.showModal({ Taro.showModal({
title: '绑定手机号失败', title: '绑定手机号失败',
content: err?.message || '绑定失败', content: err instanceof Error ? err.message : '绑定失败',
confirmText: '重新登录', confirmText: '重新登录',
cancelText: '取消', cancelText: '取消',
success: (res) => { if (res.confirm) setNeedBind(false); }, success: (res) => { if (res.confirm) setNeedBind(false); },

View File

@@ -21,7 +21,7 @@ function InputField({ label, value, placeholder, type = 'digit', onChange }: {
<Text className='form-label'>{label}</Text> <Text className='form-label'>{label}</Text>
<Input <Input
className='form-input' className='form-input'
type={type as any} type={type as 'digit' | 'number' | 'text'}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
onInput={(e) => onChange(e.detail.value)} onInput={(e) => onChange(e.detail.value)}

View File

@@ -111,7 +111,7 @@ export default function PrescriptionCreate() {
<Text className='form-label'>{label}</Text> <Text className='form-label'>{label}</Text>
<Input <Input
className='form-input' className='form-input'
type={type as any} type={type as 'digit' | 'number' | 'text'}
placeholder={placeholder} placeholder={placeholder}
value={form[field]} value={form[field]}
onInput={(e) => updateField(field, e.detail.value)} onInput={(e) => updateField(field, e.detail.value)}

View File

@@ -66,9 +66,9 @@ export default function DeviceSync() {
setLastSyncAt(scheduler.getLastSyncAt()); setLastSyncAt(scheduler.getLastSyncAt());
// 检查是否有未上传的缓冲数据 // 检查是否有未上传的缓冲数据
const buffer = (bleManager as any).dataBuffer; const buffer = (bleManager as unknown as { dataBuffer?: Map<string, number> }).dataBuffer;
if (buffer) { if (buffer) {
setPendingCount(buffer.size()); setPendingCount(buffer.size);
} }
// 自动同步:超过间隔时尝试上传缓冲数据 // 自动同步:超过间隔时尝试上传缓冲数据
@@ -105,8 +105,8 @@ export default function DeviceSync() {
setErrorMsg('未发现支持的设备,请确认设备已开启蓝牙并靠近手机'); setErrorMsg('未发现支持的设备,请确认设备已开启蓝牙并靠近手机');
} }
setPageState('idle'); setPageState('idle');
} catch (e: any) { } catch (e: unknown) {
setErrorMsg(e.message || '扫描失败'); setErrorMsg(e instanceof Error ? e.message : '扫描失败');
setPageState('error'); setPageState('error');
} }
}, []); }, []);
@@ -118,8 +118,8 @@ export default function DeviceSync() {
try { try {
await getBleManager().connect(device); await getBleManager().connect(device);
setPageState('connected'); setPageState('connected');
} catch (e: any) { } catch (e: unknown) {
setErrorMsg(e.message || '连接失败'); setErrorMsg(e instanceof Error ? e.message : '连接失败');
setPageState('error'); setPageState('error');
} }
}, []); }, []);
@@ -169,8 +169,8 @@ export default function DeviceSync() {
setErrorMsg(result.error || '同步失败'); setErrorMsg(result.error || '同步失败');
setPageState('error'); setPageState('error');
} }
} catch (e: any) { } catch (e: unknown) {
setErrorMsg(e.message || '同步失败'); setErrorMsg(e instanceof Error ? e.message : '同步失败');
setPageState('error'); setPageState('error');
} }
}, [currentPatient, selectedDevice, liveReadings, returnTo]); }, [currentPatient, selectedDevice, liveReadings, returnTo]);

View File

@@ -147,7 +147,7 @@ export default function HealthInput() {
const input = BP_INDICATORS.includes(currentIndicator) const input = BP_INDICATORS.includes(currentIndicator)
? { indicator_type: currentIndicator as 'blood_pressure' | 'blood_pressure_evening', value: parseFloat(systolic), extra: { systolic: parseFloat(systolic), diastolic: parseFloat(diastolic) } } ? { indicator_type: currentIndicator as 'blood_pressure' | 'blood_pressure_evening', value: parseFloat(systolic), extra: { systolic: parseFloat(systolic), diastolic: parseFloat(diastolic) } }
: { indicator_type: currentIndicator as any, value: parseFloat(value) }; : { indicator_type: currentIndicator as 'heart_rate' | 'blood_sugar_fasting' | 'blood_sugar_postprandial' | 'weight' | 'temperature', value: parseFloat(value) };
const valueResult = valueCheck.safeParse(input.value); const valueResult = valueCheck.safeParse(input.value);
if (!valueResult.ok) { if (!valueResult.ok) {

View File

@@ -43,8 +43,8 @@ export default function EventsPage() {
await pointsApi.registerEvent(event.id); await pointsApi.registerEvent(event.id);
Taro.showToast({ title: '报名成功', icon: 'success' }); Taro.showToast({ title: '报名成功', icon: 'success' });
loadEvents(); loadEvents();
} catch (err: any) { } catch (err: unknown) {
const msg = err?.message || '报名失败'; const msg = err instanceof Error ? err.message : '报名失败';
Taro.showToast({ title: msg.substring(0, 20), icon: 'none' }); Taro.showToast({ title: msg.substring(0, 20), icon: 'none' });
} finally { } finally {
setRegistering(null); setRegistering(null);

View File

@@ -55,7 +55,7 @@ export default function FollowUpDetail() {
trackEvent('followup_submit', { task_id: id }); trackEvent('followup_submit', { task_id: id });
const tmplId = TEMPLATE_IDS.FOLLOWUP_REMINDER; const tmplId = TEMPLATE_IDS.FOLLOWUP_REMINDER;
if (tmplId) { if (tmplId) {
try { await (Taro.requestSubscribeMessage as any)({ tmplIds: [tmplId] }); } catch { /* 用户拒绝 */ } try { await (Taro as { requestSubscribeMessage: (opts: { tmplIds: string[] }) => Promise<unknown> }).requestSubscribeMessage({ tmplIds: [tmplId] }); } catch { /* 用户拒绝 */ }
} }
setContent(''); setContent('');
} catch (err) { } catch (err) {

View File

@@ -7,6 +7,10 @@ import type {
NormalizedReading, NormalizedReading,
SyncResult, SyncResult,
BLEManagerConfig, BLEManagerConfig,
BLEScanResult,
BLEConnectionChangeResult,
BLECharacteristicChangeResult,
BLEServiceItem,
} from './types'; } from './types';
import { DataBuffer } from './DataBuffer'; import { DataBuffer } from './DataBuffer';
@@ -27,8 +31,8 @@ export class BLEManager {
private scanTimer: ReturnType<typeof setTimeout> | null = null; private scanTimer: ReturnType<typeof setTimeout> | null = null;
private onConnectionChange?: (state: BLEConnectionState) => void; private onConnectionChange?: (state: BLEConnectionState) => void;
private onReadings?: (readings: NormalizedReading[]) => void; private onReadings?: (readings: NormalizedReading[]) => void;
private connChangeHandler: ((res: any) => void) | null = null; private connChangeHandler: ((res: BLEConnectionChangeResult) => void) | null = null;
private charChangeHandler: ((res: any) => void) | null = null; private charChangeHandler: ((res: BLECharacteristicChangeResult) => void) | null = null;
constructor(config?: Partial<BLEManagerConfig>) { constructor(config?: Partial<BLEManagerConfig>) {
this.config = { ...DEFAULT_CONFIG, ...config }; this.config = { ...DEFAULT_CONFIG, ...config };
@@ -70,8 +74,9 @@ export class BLEManager {
async initialize(): Promise<void> { async initialize(): Promise<void> {
try { try {
await Taro.openBluetoothAdapter(); await Taro.openBluetoothAdapter();
} catch (e: any) { } catch (e: unknown) {
throw new Error(e.errMsg || '蓝牙初始化失败,请检查蓝牙是否开启'); const errMsg = e instanceof Error ? e.message : (e as { errMsg?: string })?.errMsg || '蓝牙初始化失败,请检查蓝牙是否开启';
throw new Error(errMsg);
} }
} }
@@ -81,7 +86,7 @@ export class BLEManager {
const discovered = new Map<string, BLEDevice>(); const discovered = new Map<string, BLEDevice>();
const onFound = (res: any) => { const onFound = (res: BLEScanResult) => {
for (const device of res.devices || []) { for (const device of res.devices || []) {
const name = device.name || device.localName || ''; const name = device.name || device.localName || '';
if (!name) continue; if (!name) continue;
@@ -90,7 +95,7 @@ export class BLEManager {
discovered.set(device.deviceId, { discovered.set(device.deviceId, {
deviceId: device.deviceId, deviceId: device.deviceId,
name, name,
RSSI: device.RSSI, RSSI: device.RSSI ?? 0,
localName: device.localName, localName: device.localName,
advertisData: device.advertisData, advertisData: device.advertisData,
adapter, adapter,
@@ -156,7 +161,7 @@ export class BLEManager {
} }
// 监听断连 // 监听断连
this.connChangeHandler = (res: any) => { this.connChangeHandler = (res: BLEConnectionChangeResult) => {
if (res.deviceId === device.deviceId && !res.connected) { if (res.deviceId === device.deviceId && !res.connected) {
this.updateState('disconnected', '设备断开连接'); this.updateState('disconnected', '设备断开连接');
this.connection = null; this.connection = null;
@@ -170,7 +175,7 @@ export class BLEManager {
// 启用通知 // 启用通知
for (const { service: svcUUID, characteristic: charUUID } of device.adapter.notifyCharacteristics) { for (const { service: svcUUID, characteristic: charUUID } of device.adapter.notifyCharacteristics) {
const svc = services.find((s: any) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase())); const svc = services.find((s: BLEServiceItem) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase()));
if (!svc) continue; if (!svc) continue;
await Taro.getBLEDeviceCharacteristics({ await Taro.getBLEDeviceCharacteristics({
@@ -187,7 +192,7 @@ export class BLEManager {
} }
// 监听数据通知 // 监听数据通知
this.charChangeHandler = (res: any) => { this.charChangeHandler = (res: BLECharacteristicChangeResult) => {
if (res.deviceId !== device.deviceId) return; if (res.deviceId !== device.deviceId) return;
const newReadings = device.adapter!.parseNotification( const newReadings = device.adapter!.parseNotification(
res.serviceId, res.serviceId,
@@ -207,10 +212,12 @@ export class BLEManager {
this.connection = { ...this.connection, state: 'connected', connectedAt: Date.now() }; this.connection = { ...this.connection, state: 'connected', connectedAt: Date.now() };
this.updateState('connected'); this.updateState('connected');
} catch (e: any) { } catch (e: unknown) {
this.updateState('error', e.errMsg || e.message || '连接失败'); const errMsg = (e as { errMsg?: string })?.errMsg;
const msg = errMsg || (e instanceof Error ? e.message : '') || '连接失败';
this.updateState('error', msg);
this.connection = null; this.connection = null;
throw new Error(e.errMsg || '蓝牙连接失败'); throw new Error(errMsg || '蓝牙连接失败');
} }
} }
@@ -227,7 +234,7 @@ export class BLEManager {
const services = servicesRes.services || []; const services = servicesRes.services || [];
for (const { service: svcUUID, characteristic: charUUID } of adapter.readCharacteristics) { for (const { service: svcUUID, characteristic: charUUID } of adapter.readCharacteristics) {
const svc = services.find((s: any) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase())); const svc = services.find((s: BLEServiceItem) => s.uuid.toUpperCase().includes(svcUUID.toUpperCase()));
if (!svc) continue; if (!svc) continue;
try { try {
@@ -274,8 +281,8 @@ export class BLEManager {
readingsCount: batch.length, readingsCount: batch.length,
uploadedCount: uploaded, uploadedCount: uploaded,
}; };
} catch (e: any) { } catch (e: unknown) {
lastError = e.message || '上传失败'; lastError = e instanceof Error ? e.message : '上传失败';
// flush 已取出,失败时需要放回 // flush 已取出,失败时需要放回
this.dataBuffer.push(batch); this.dataBuffer.push(batch);
} }

View File

@@ -97,3 +97,34 @@ export type GenericBLEProfile =
| 'heart_rate' // Heart Rate Service (0x180D) | 'heart_rate' // Heart Rate Service (0x180D)
| 'health_thermometer' // Health Thermometer Service (0x1809) | 'health_thermometer' // Health Thermometer Service (0x1809)
| 'blood_pressure'; // Blood Pressure Service (0x1810) | 'blood_pressure'; // Blood Pressure Service (0x1810)
/** 微信 BLE 扫描回调结果 */
export interface BLEScanResult {
devices: Array<{
deviceId: string;
name?: string;
RSSI?: number;
localName?: string;
advertisData?: ArrayBuffer;
}>;
}
/** 微信 BLE 连接状态变更回调结果 */
export interface BLEConnectionChangeResult {
deviceId: string;
connected: boolean;
}
/** 微信 BLE 特征值变更回调结果 */
export interface BLECharacteristicChangeResult {
deviceId: string;
serviceId: string;
characteristicId: string;
value: ArrayBuffer;
}
/** 微信 BLE 服务描述getBLEDeviceServices 返回项) */
export interface BLEServiceItem {
uuid: string;
isPrimary?: boolean;
}

View File

@@ -243,10 +243,10 @@ async function request<T>(method: string, path: string, data?: unknown, timeout?
const url = `${BASE_URL}${path}`; const url = `${BASE_URL}${path}`;
let res: Taro.request.SuccessCallbackResult; let res: Taro.request.SuccessCallbackResult;
try { try {
res = await Taro.request({ url, method: method as any, data, header: headers, timeout: timeout || 15000 }); res = await Taro.request({ url, method: method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS', data, header: headers, timeout: timeout || 15000 });
} catch (err: any) { } catch (err: unknown) {
if (signal?.aborted) throw new Error('请求已取消'); if (signal?.aborted) throw new Error('请求已取消');
const msg = err?.errMsg || ''; const msg = (err as { errMsg?: string })?.errMsg || '';
if (msg.includes('timeout')) { if (msg.includes('timeout')) {
Taro.showToast({ title: '网络超时,请重试', icon: 'none' }); Taro.showToast({ title: '网络超时,请重试', icon: 'none' });
throw new Error('网络超时'); throw new Error('网络超时');