fix(miniprogram): 审计修复 — P0/P1 共 16 个问题
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

P0 功能阻断:
- 修复 login→bindPhone openid 状态传递断裂
- 首页健康卡片对接 useHealthStore 真实数据
- 血压录入改为收缩压/舒张压双输入
- 快捷服务路径修正(报告→/pages/report、随访→/pages/followup)

P1 类型安全 + 组件:
- 替换所有 <input>/<image>/<textarea> 为 Taro 组件
- service 层 any 类型全部替换(Doctor/DoctorSchedule/IndicatorDetail/FollowUpContent/PatientUpdateInput)
- 预约详情数据传递简化为纯 Storage 缓存
- Article 接口添加 author 字段
This commit is contained in:
iven
2026-04-24 01:37:34 +08:00
parent 6fbe7ec530
commit 7b7677dfec
16 changed files with 171 additions and 87 deletions

View File

@@ -17,28 +17,9 @@ export default function AppointmentDetail() {
const id = router.params.id || '';
const [cancelling, setCancelling] = useState(false);
// 从页面参数或全局缓存获取预约数据
const encodedData = router.params.data || '';
let appointment: Appointment | null = null;
try {
if (encodedData) {
appointment = JSON.parse(decodeURIComponent(encodedData));
}
} catch {
// 解析失败则尝试从 Storage 获取
const cached = Taro.getStorageSync('appointment_detail_cache');
if (cached && cached.id === id) {
appointment = cached;
}
}
// 如果没有传数据,尝试从缓存获取
if (!appointment) {
const cached = Taro.getStorageSync('appointment_detail_cache');
if (cached && cached.id === id) {
appointment = cached;
}
}
// 从缓存获取预约数据
const cached = Taro.getStorageSync('appointment_detail_cache');
const appointment: Appointment | null = (cached && cached.id === id) ? cached : null;
const status = appointment ? (STATUS_MAP[appointment.status] || { label: appointment.status, className: 'tag-pending' }) : { label: '未知', className: 'tag-pending' };
const canCancel = appointment && (appointment.status === 'pending' || appointment.status === 'confirmed');

View File

@@ -1,5 +1,5 @@
import React, { useState, useCallback } from 'react';
import { View, Text } from '@tarojs/components';
import { View, Text, Image } from '@tarojs/components';
import Taro, { useDidShow, usePullDownRefresh, useReachBottom } from '@tarojs/taro';
import { listArticles, Article } from '../../services/article';
import EmptyState from '../../components/EmptyState';
@@ -74,7 +74,7 @@ export default function ArticleList() {
</View>
{a.cover_image && (
<View className='article-card-cover'>
<image className='cover-img' src={a.cover_image} mode='aspectFill' />
<Image className='cover-img' src={a.cover_image} mode='aspectFill' />
</View>
)}
</View>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { View, Text } from '@tarojs/components';
import { View, Text, Textarea } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { listTasks, submitRecord, FollowUpTask } from '../../../services/followup';
import './index.scss';
@@ -97,7 +97,7 @@ export default function FollowUpDetail() {
{!isCompleted && (
<View className='submit-card'>
<Text className='section-title'>访</Text>
<textarea
<Textarea
className='submit-textarea'
placeholder='请输入随访内容...'
value={content}

View File

@@ -17,27 +17,46 @@ const INDICATORS = [
export default function HealthInput() {
const [indicatorIdx, setIndicatorIdx] = useState(0);
const [value, setValue] = useState('');
const [systolic, setSystolic] = useState('');
const [diastolic, setDiastolic] = useState('');
const [note, setNote] = useState('');
const [submitting, setSubmitting] = useState(false);
const { currentPatient } = useAuthStore();
const handleSubmit = async () => {
if (!value) {
Taro.showToast({ title: '请输入数值', icon: 'none' });
return;
}
if (!currentPatient) {
Taro.showToast({ title: '请先选择就诊人', icon: 'none' });
return;
}
const currentIndicator = INDICATORS[indicatorIdx].value;
setSubmitting(true);
try {
await inputVitalSign(currentPatient.id, {
indicator_type: INDICATORS[indicatorIdx].value,
value: parseFloat(value),
note: note || undefined,
});
if (currentIndicator === 'blood_pressure') {
if (!systolic || !diastolic) {
Taro.showToast({ title: '请输入收缩压和舒张压', icon: 'none' });
setSubmitting(false);
return;
}
await inputVitalSign(currentPatient.id, {
indicator_type: 'blood_pressure',
value: parseFloat(systolic),
extra: { systolic: parseFloat(systolic), diastolic: parseFloat(diastolic) },
note: note || undefined,
});
} else {
if (!value) {
Taro.showToast({ title: '请输入数值', icon: 'none' });
setSubmitting(false);
return;
}
await inputVitalSign(currentPatient.id, {
indicator_type: currentIndicator,
value: parseFloat(value),
note: note || undefined,
});
}
Taro.showToast({ title: '录入成功', icon: 'success' });
setTimeout(() => Taro.navigateBack(), 1000);
} catch (e: any) {
@@ -64,16 +83,41 @@ export default function HealthInput() {
</Picker>
</View>
<View className='input-section'>
<Text className='input-label'></Text>
<Input
type='digit'
className='input-field'
placeholder='请输入数值'
value={value}
onInput={(e) => setValue(e.detail.value)}
/>
</View>
{INDICATORS[indicatorIdx].value === 'blood_pressure' ? (
<>
<View className='input-section'>
<Text className='input-label'></Text>
<Input
type='digit'
className='input-field'
placeholder='如 120'
value={systolic}
onInput={(e) => setSystolic(e.detail.value)}
/>
</View>
<View className='input-section'>
<Text className='input-label'></Text>
<Input
type='digit'
className='input-field'
placeholder='如 80'
value={diastolic}
onInput={(e) => setDiastolic(e.detail.value)}
/>
</View>
</>
) : (
<View className='input-section'>
<Text className='input-label'></Text>
<Input
type='digit'
className='input-field'
placeholder='请输入数值'
value={value}
onInput={(e) => setValue(e.detail.value)}
/>
</View>
)}
<View className='input-section'>
<Text className='input-label'></Text>

View File

@@ -1,16 +1,21 @@
import { View, Text } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { useAuthStore } from '../../stores/auth';
import { useHealthStore } from '../../stores/health';
import EmptyState from '../../components/EmptyState';
import './index.scss';
export default function Index() {
const { user, restore } = useAuthStore();
const { todaySummary, refreshToday } = useHealthStore();
useDidShow(() => {
restore();
refreshToday();
});
const s = todaySummary || {};
const greeting = () => {
const h = new Date().getHours();
if (h < 6) return '凌晨好';
@@ -38,10 +43,10 @@ export default function Index() {
<Text className='section-title'></Text>
<View className='health-grid'>
{[
{ label: '血压', value: '--/--', unit: 'mmHg', status: '' },
{ label: '心率', value: '--', unit: 'bpm', status: '' },
{ label: '血糖', value: '--', unit: 'mmol/L', status: '' },
{ label: '体重', value: '--', unit: 'kg', status: '' },
{ label: '血压', value: s.blood_pressure ? `${s.blood_pressure.systolic}/${s.blood_pressure.diastolic}` : '--/--', unit: 'mmHg' },
{ label: '心率', value: s.heart_rate ? `${s.heart_rate.value}` : '--', unit: 'bpm' },
{ label: '血糖', value: s.blood_sugar ? `${s.blood_sugar.value}` : '--', unit: 'mmol/L' },
{ label: '体重', value: s.weight ? `${s.weight.value}` : '--', unit: 'kg' },
].map((item) => (
<View className='health-item' key={item.label}>
<Text className='health-label'>{item.label}</Text>
@@ -57,15 +62,21 @@ export default function Index() {
<Text className='section-title'></Text>
<View className='service-grid'>
{[
{ label: '录数据', icon: '📝', path: '/pages/health/index' },
{ label: '预约', icon: '📅', path: '/pages/appointment/index' },
{ label: '报告', icon: '📋', path: '/pages/profile/index' },
{ label: '随访', icon: '💬', path: '/pages/profile/index' },
{ label: '录数据', icon: '📝', path: '/pages/health/index', isTab: true },
{ label: '预约', icon: '📅', path: '/pages/appointment/index', isTab: true },
{ label: '报告', icon: '📋', path: '/pages/report/index', isTab: false },
{ label: '随访', icon: '💬', path: '/pages/followup/index', isTab: false },
].map((item) => (
<View
className='service-item'
key={item.label}
onClick={() => Taro.switchTab({ url: item.path })}
onClick={() => {
if (item.isTab) {
Taro.switchTab({ url: item.path });
} else {
Taro.navigateTo({ url: item.path });
}
}}
>
<Text className='service-icon'>{item.icon}</Text>
<Text className='service-label'>{item.label}</Text>

View File

@@ -6,7 +6,6 @@ import './index.scss';
export default function Login() {
const [needBind, setNeedBind] = useState(false);
const [openid, setOpenid] = useState('');
const { login, bindPhone, loading } = useAuthStore();
const handleWechatLogin = async () => {
@@ -16,11 +15,10 @@ export default function Login() {
if (success) {
Taro.switchTab({ url: '/pages/index/index' });
} else {
// 未绑定,需要获取手机号
// 未绑定,需要获取手机号openid 已由 store 缓存到 Storage
setNeedBind(true);
// 从最近的登录响应获取 openid简化处理
}
} catch (e: any) {
} catch {
Taro.showToast({ title: '登录失败', icon: 'none' });
}
};
@@ -31,7 +29,7 @@ export default function Login() {
return;
}
const { encryptedData, iv } = e.detail;
const success = await bindPhone(openid, encryptedData, iv);
const success = await bindPhone(encryptedData, iv);
if (success) {
Taro.switchTab({ url: '/pages/index/index' });
} else {

View File

@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { View, Text } from '@tarojs/components';
import { View, Text, Input, Picker } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { Picker } from '@tarojs/components';
import { createPatient } from '../../../services/patient';
import './index.scss';
@@ -44,7 +43,7 @@ export default function FamilyAdd() {
{/* 姓名 */}
<View className='form-item'>
<Text className='form-label'></Text>
<input
<Input
className='form-input'
placeholder='请输入姓名'
value={name}

View File

@@ -2,6 +2,8 @@ import React, { useState, useCallback } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { listTasks, FollowUpTask } from '../../../services/followup';
import EmptyState from '../../../components/EmptyState';
import Loading from '../../../components/Loading';
import './index.scss';
const TABS = [
@@ -87,18 +89,14 @@ export default function MyFollowUps() {
</View>
{tasks.length === 0 && !loading && (
<View className='empty-state'>
<Text className='empty-text'>{(() => {
const tab = TABS.find((t) => t.key === activeTab);
return tab ? tab.label : '';
})()}</Text>
</View>
<EmptyState text={`暂无${(() => {
const tab = TABS.find((t) => t.key === activeTab);
return tab ? tab.label : '';
})()}任务`} />
)}
{loading && (
<View className='loading-hint'>
<Text className='loading-text'>...</Text>
</View>
<Loading />
)}
</View>
);

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { View, Text } from '@tarojs/components';
import { View, Text, Input } from '@tarojs/components';
import Taro from '@tarojs/taro';
import EmptyState from '../../../components/EmptyState';
import './index.scss';
interface MedicationReminder {
@@ -105,9 +106,7 @@ export default function MedicationReminder() {
</View>
{reminders.length === 0 && (
<View className='empty-state'>
<Text className='empty-text'></Text>
</View>
<EmptyState text='暂无用药提醒' />
)}
{/* 添加表单 */}
@@ -115,7 +114,7 @@ export default function MedicationReminder() {
<View className='form-card'>
<View className='form-item'>
<Text className='form-label'></Text>
<input
<Input
className='form-input'
placeholder='请输入药品名称'
value={formName}
@@ -124,7 +123,7 @@ export default function MedicationReminder() {
</View>
<View className='form-item'>
<Text className='form-label'></Text>
<input
<Input
className='form-input'
placeholder='如1片、10ml'
value={formDosage}