perf(mp): 移除 Zod 依赖,轻量验证替代 — 包体积 -300KB

- 新增 utils/validate.ts 轻量验证工具(<1KB vs Zod 360KB)
- daily-monitoring: Zod schema → validateNum() 直接验证
- input: Zod schema → num()/validateStr() 直接验证
- config/index.ts: 移除 Zod include 编译配置

效果:总体积 1.8MB→1.5MB(-17%),pkg-health 分包 432KB→84KB(-81%)
This commit is contained in:
iven
2026-05-13 23:56:12 +08:00
parent 0f6f7a2851
commit 9faccac9eb
4 changed files with 88 additions and 37 deletions

View File

@@ -33,9 +33,7 @@ export default defineConfig(async (merge) => {
mini: {
compile: {
exclude: [],
include: [
require.resolve('zod').replace(/[\\/]index\.cjs$/, ''),
],
include: [],
},
postcss: {
pxtransform: { enable: true, config: {} },

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { View, Text, Input, Picker } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { z } from 'zod';
import { validateNum } from '@/utils/validate';
import { createDailyMonitoring } from '@/services/health';
import { useAuthStore } from '@/stores/auth';
import { useHealthStore } from '@/stores/health';
@@ -11,10 +11,10 @@ import { trackEvent } from '@/services/analytics';
import { useElderClass } from '../../../hooks/useElderClass';
import './index.scss';
const bpSchema = z.number().min(30, '血压值不能低于30').max(300, '血压值不能高于300').optional();
const weightSchema = z.number().min(1, '体重不能低于1kg').max(500, '体重不能高于500kg').optional();
const bloodSugarSchema = z.number().min(0.1, '血糖值不能低于0.1').max(50, '血糖值不能高于50').optional();
const volumeSchema = z.number().min(0, '数值不能为负').max(10000, '数值超出合理范围').optional();
const BP_RANGE = { min: 30, minMsg: '血压值不能低于30', max: 300, maxMsg: '血压值不能高于300', optional: true };
const WEIGHT_RANGE = { min: 1, minMsg: '体重不能低于1kg', max: 500, maxMsg: '体重不能高于500kg', optional: true };
const SUGAR_RANGE = { min: 0.1, minMsg: '血糖值不能低于0.1', max: 50, maxMsg: '血糖值不能高于50', optional: true };
const VOLUME_RANGE = { min: 0, minMsg: '数值不能为负', max: 10000, maxMsg: '数值超出合理范围', optional: true };
function formatDate(date: Date): string {
const y = date.getFullYear();
@@ -172,24 +172,22 @@ export default function DailyMonitoring() {
urineOutput: parseNum(urineOutput),
};
const validations: Array<[z.ZodTypeAny, number | undefined, string]> = [
[bpSchema, fields.morningSystolic, '晨起收缩压'],
[bpSchema, fields.morningDiastolic, '晨起舒张压'],
[bpSchema, fields.eveningSystolic, '晚间收缩压'],
[bpSchema, fields.eveningDiastolic, '晚间舒张压'],
[weightSchema, fields.weight, '体重'],
[bloodSugarSchema, fields.bloodSugar, '血糖'],
[volumeSchema, fields.fluidIntake, '饮水量'],
[volumeSchema, fields.urineOutput, '尿量'],
const validations: Array<[number | undefined, string, typeof BP_RANGE]> = [
[fields.morningSystolic, '晨起收缩压', BP_RANGE],
[fields.morningDiastolic, '晨起舒张压', BP_RANGE],
[fields.eveningSystolic, '晚间收缩压', BP_RANGE],
[fields.eveningDiastolic, '晚间舒张压', BP_RANGE],
[fields.weight, '体重', WEIGHT_RANGE],
[fields.bloodSugar, '血糖', SUGAR_RANGE],
[fields.fluidIntake, '饮水量', VOLUME_RANGE],
[fields.urineOutput, '尿量', VOLUME_RANGE],
];
for (const [schema, value, label] of validations) {
if (value !== undefined) {
const result = schema.safeParse(value);
if (!result.success) {
Taro.showToast({ title: `${label}: ${result.error.errors[0].message}`, icon: 'none' });
return;
}
for (const [value, label, range] of validations) {
const err = validateNum(value, label, range);
if (err) {
Taro.showToast({ title: err, icon: 'none' });
return;
}
}

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { View, Text, Input, Picker } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { z } from 'zod';
import { num, validateStr } from '@/utils/validate';
import { inputVitalSign, getHealthThresholds, findThreshold, DEFAULT_THRESHOLDS, type HealthThreshold } from '../../../services/health';
import { useAuthStore } from '../../../stores/auth';
import { useHealthStore } from '@/stores/health';
@@ -23,15 +23,9 @@ const INDICATORS = [
const BP_INDICATORS = ['blood_pressure', 'blood_pressure_evening'];
const vitalSignSchema = z.object({
indicator_type: z.enum(['blood_pressure', 'blood_pressure_evening', 'heart_rate', 'blood_sugar_fasting', 'blood_sugar_postprandial', 'weight', 'temperature']),
value: z.number().positive({ message: '请输入有效数值' }),
extra: z.object({
systolic: z.number().min(60, '收缩压过低').max(250, '收缩压过高,请及时就医').optional(),
diastolic: z.number().min(40, '舒张压过低').max(150, '舒张压过高,请及时就医').optional(),
}).optional(),
note: z.string().max(200, '备注不能超过200字').optional(),
});
const valueCheck = num({ posMsg: '请输入有效数值' });
const systolicCheck = num({ min: 60, minMsg: '收缩压过低', max: 250, maxMsg: '收缩压过高,请及时就医', optional: true });
const diastolicCheck = num({ min: 40, minMsg: '舒张压过低', max: 150, maxMsg: '舒张压过高,请及时就医', optional: true });
/** 根据动态阈值生成警告配置 */
function getWarnForIndicator(
@@ -120,11 +114,23 @@ export default function HealthInput() {
? { 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) };
const result = vitalSignSchema.safeParse(input);
if (!result.success) {
Taro.showToast({ title: result.error.issues[0].message, icon: 'none' });
const valueResult = valueCheck.safeParse(input.value);
if (!valueResult.ok) {
Taro.showToast({ title: valueResult.message, icon: 'none' });
return;
}
if (input.extra?.systolic !== undefined) {
const r = systolicCheck.safeParse(input.extra.systolic);
if (!r.ok) { Taro.showToast({ title: r.message, icon: 'none' }); return; }
}
if (input.extra?.diastolic !== undefined) {
const r = diastolicCheck.safeParse(input.extra.diastolic);
if (!r.ok) { Taro.showToast({ title: r.message, icon: 'none' }); return; }
}
if (note) {
const err = validateStr(note, 200, '备注');
if (err) { Taro.showToast({ title: err, icon: 'none' }); return; }
}
const threshold = getWarnForIndicator(thresholds, currentIndicator);
if (threshold) {

View File

@@ -0,0 +1,49 @@
interface NumRule {
min?: number;
max?: number;
minMsg?: string;
maxMsg?: string;
posMsg?: string;
optional?: boolean;
}
interface ValidateResult {
ok: boolean;
message: string;
}
export function num(rule: NumRule) {
return {
safeParse(value: number | undefined): ValidateResult {
if (value === undefined || value === null) {
return rule.optional ? { ok: true, message: '' } : { ok: false, message: posMsg || '请输入有效数值' };
}
if (isNaN(value)) return { ok: false, message: '请输入有效数值' };
if (rule.min !== undefined && value < rule.min) return { ok: false, message: rule.minMsg || `数值不能低于${rule.min}` };
if (rule.max !== undefined && value > rule.max) return { ok: false, message: rule.maxMsg || `数值不能高于${rule.max}` };
return { ok: true, message: '' };
},
};
}
interface FieldRule {
min?: number;
max?: number;
minMsg?: string;
maxMsg?: string;
optional?: boolean;
}
export function validateNum(value: number | undefined, label: string, rule: FieldRule): string | null {
if (value === undefined || value === null) return rule.optional ? null : `${label}: 请输入有效数值`;
if (isNaN(value)) return `${label}: 请输入有效数值`;
if (rule.min !== undefined && value < rule.min) return `${label}: ${rule.minMsg ?? `数值不能低于${rule.min}`}`;
if (rule.max !== undefined && value > rule.max) return `${label}: ${rule.maxMsg ?? `数值不能高于${rule.max}`}`;
return null;
}
export function validateStr(value: string | undefined, maxLen: number, label: string): string | null {
if (!value) return null;
if (value.length > maxLen) return `${label}不能超过${maxLen}`;
return null;
}