Files
hms/apps/miniprogram/src/pages/pkg-profile/followups/detail/index.tsx
iven c9fe654d44 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 测试通过
2026-05-22 08:30:01 +08:00

163 lines
5.3 KiB
TypeScript

import { useState, useCallback } from 'react';
import { View, Text, Textarea } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { usePageData } from '@/hooks/usePageData';
import { getTaskDetail, submitRecord } from '@/services/followup';
import type { FollowUpTask } from '@/services/followup';
import { TEMPLATE_IDS } from '@/services/wechat-templates';
import { trackEvent } from '@/services/analytics';
import Loading from '@/components/Loading';
import ErrorState from '@/components/ErrorState';
import PageShell from '@/components/ui/PageShell';
import ContentCard from '@/components/ui/ContentCard';
import { useElderClass } from '@/hooks/useElderClass';
import './index.scss';
export default function FollowUpDetail() {
const modeClass = useElderClass();
const router = useRouter();
const id = router.params.id || '';
const [task, setTask] = useState<FollowUpTask | null>(null);
const [content, setContent] = useState('');
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const fetchTask = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const data = await getTaskDetail(id);
setTask(data);
} catch (err) {
console.error('[FollowUpDetail]', err);
setError(true);
} finally {
setLoading(false);
}
}, [id]);
usePageData(fetchTask, { throttleMs: 60000 });
const handleSubmit = async () => {
if (!content.trim()) {
Taro.showToast({ title: '请输入内容', icon: 'none' });
return;
}
setSubmitting(true);
try {
await submitRecord(id, {
result: content.trim(),
patient_condition: content.trim(),
});
Taro.showToast({ title: '提交成功', icon: 'success' });
trackEvent('followup_submit', { task_id: id });
const tmplId = TEMPLATE_IDS.FOLLOWUP_REMINDER;
if (tmplId) {
try { await (Taro as { requestSubscribeMessage: (opts: { tmplIds: string[] }) => Promise<unknown> }).requestSubscribeMessage({ tmplIds: [tmplId] }); } catch { /* 用户拒绝 */ }
}
setContent('');
} catch (err) {
console.warn('[followup] 提交失败:', err);
Taro.showToast({ title: '提交失败', icon: 'none' });
} finally {
setSubmitting(false);
}
};
const getStatusLabel = (status: string) => {
if (status === 'completed') return '已完成';
if (status === 'overdue') return '已过期';
return '待完成';
};
const getStatusClass = (status: string) => {
if (status === 'completed') return 'status-completed';
if (status === 'overdue') return 'status-overdue';
return 'status-pending';
};
const getCountdown = (dueDate: string, status: string) => {
if (status === 'completed') return null;
const now = new Date();
const due = new Date(dueDate);
const diffMs = due.getTime() - now.getTime();
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
if (diffDays < 0) return { text: `已过期 ${Math.abs(diffDays)}`, urgent: true };
if (diffDays === 0) return { text: '今天截止', urgent: true };
if (diffDays <= 3) return { text: `还剩 ${diffDays}`, urgent: true };
return { text: `还剩 ${diffDays}`, urgent: false };
};
if (loading) {
return (
<PageShell className={modeClass}>
<Loading />
</PageShell>
);
}
if (error || !task) {
return (
<PageShell className={modeClass}>
<ErrorState text='任务不存在' />
</PageShell>
);
}
const isCompleted = task.status === 'completed';
return (
<PageShell className={modeClass}>
<ContentCard>
<Text className='detail-title'>{task.follow_up_type}</Text>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className={`detail-value ${getStatusClass(task.status)}`}>
{getStatusLabel(task.status)}
</Text>
</View>
<View className='detail-row'>
<Text className='detail-label'></Text>
<Text className='detail-value'>{task.planned_date}</Text>
</View>
{(() => {
const cd = getCountdown(task.planned_date, task.status);
return cd ? (
<View className={`countdown ${cd.urgent ? 'countdown-urgent' : ''}`}>
<Text className='countdown-text'>{cd.text}</Text>
</View>
) : null;
})()}
{task.content_template && (
<View className='detail-desc'>
<Text className='detail-desc-text'>{task.content_template}</Text>
</View>
)}
</ContentCard>
{!isCompleted && (
<ContentCard>
<Text className='section-title'>访</Text>
<Textarea
className='submit-textarea'
placeholder='请输入随访内容...'
value={content}
onInput={(e) => setContent(e.detail.value)}
maxlength={500}
/>
<View
className={`submit-btn ${submitting ? 'disabled' : ''}`}
onClick={submitting ? undefined : handleSubmit}
>
<Text className='submit-btn-text'>
{submitting ? '提交中...' : '提交'}
</Text>
</View>
</ContentCard>
)}
</PageShell>
);
}