feat(ai): Phase 2B 洞察→推送→反馈闭环 — 风险评分+通知+建议反馈

- 风险评分引擎 load_patient_data 实装(体征+化验异常)
- refresh_all_patients 高风险自动创建洞察+事件推送
- erp-message 订阅 copilot.insight.created 推送医护通知
- 每日 cron 增加洞察过期清理+建议过期清理
- POST /ai/suggestions/{id}/feedback 建议反馈端点
- SuggestionFeedbackService 反馈服务层
- 小程序健康页建议卡片增加采纳/忽略/咨询医生按钮
This commit is contained in:
iven
2026-05-19 01:19:09 +08:00
parent 2660f1afff
commit 9576e80175
10 changed files with 504 additions and 32 deletions

View File

@@ -289,10 +289,22 @@
}
.ai-suggestion-item {
padding: var(--tk-gap-xs) 0;
border-bottom: 1px solid rgba($acc, 0.15);
&:last-child {
border-bottom: none;
}
}
.ai-suggestion-main {
display: flex;
align-items: center;
gap: var(--tk-gap-xs);
padding: var(--tk-gap-2xs) 0;
&:active {
opacity: var(--tk-touch-feedback-opacity);
}
}
.ai-risk-dot {
@@ -319,3 +331,48 @@
color: $tx2;
line-height: 1.6;
}
/* ─── AI 建议反馈按钮 ─── */
.ai-feedback-row {
display: flex;
gap: var(--tk-gap-xs);
margin-top: var(--tk-gap-2xs);
padding-left: 20px;
}
.ai-feedback-btn {
height: 32px;
border-radius: $r-xs;
@include flex-center;
padding: 0 var(--tk-gap-sm);
&:active {
opacity: var(--tk-touch-feedback-opacity);
}
&.ai-feedback-adopt {
background: rgba($acc, 0.15);
}
&.ai-feedback-ignore {
background: $surface-alt;
}
&.ai-feedback-consult {
background: var(--tk-pri-l);
}
}
.ai-feedback-btn-text {
font-size: var(--tk-font-micro);
font-weight: 500;
color: $tx2;
}
.ai-feedback-adopt .ai-feedback-btn-text {
color: $acc;
}
.ai-feedback-consult .ai-feedback-btn-text {
color: var(--tk-pri);
}

View File

@@ -12,6 +12,7 @@ import SegmentTabs from '../../components/SegmentTabs';
import PageShell from '@/components/ui/PageShell';
import ContentCard from '@/components/ui/ContentCard';
import { useHealthData, VITAL_TABS, type VitalType } from './useHealthData';
import { submitSuggestionFeedback } from '../../services/ai-analysis';
import './index.scss';
function buildRefRange(t: HealthThreshold[]): Record<VitalType, string> {
@@ -171,16 +172,7 @@ export default function Health() {
</View>
{aiSuggestions.length > 0 && (
<View className='ai-suggestion-card' onClick={() => {
const first = aiSuggestions[0];
if (first?.suggestion_type === 'appointment') {
safeNavigateTo(`/pages/appointment/create/index`);
} else if (first?.suggestion_type === 'followup') {
safeNavigateTo('/pages/pkg-profile/followups/index');
} else {
Taro.switchTab({ url: '/pages/health/index' });
}
}}>
<View className='ai-suggestion-card'>
<View className='ai-card-header'>
<Text className='ai-card-title'>AI </Text>
<Text className='ai-card-count'>{aiSuggestions.length} </Text>
@@ -192,8 +184,44 @@ export default function Health() {
const reason = (params?.reason as string) || (params?.message as string) || typeLabel;
return (
<View key={s.id} className='ai-suggestion-item'>
<View className={`ai-risk-dot ${riskCls}`} />
<Text className='ai-suggestion-text'>{reason.slice(0, 40)}</Text>
<View className='ai-suggestion-main' onClick={() => {
if (s.suggestion_type === 'appointment') {
safeNavigateTo(`/pages/appointment/create/index`);
} else if (s.suggestion_type === 'followup') {
safeNavigateTo('/pages/pkg-profile/followups/index');
}
}}>
<View className={`ai-risk-dot ${riskCls}`} />
<Text className='ai-suggestion-text'>{reason.slice(0, 40)}</Text>
</View>
<View className='ai-feedback-row'>
<View className='ai-feedback-btn ai-feedback-adopt' onClick={async () => {
try {
await submitSuggestionFeedback(s.id, 'adopt');
Taro.showToast({ title: '已采纳', icon: 'success' });
fetchData();
} catch { Taro.showToast({ title: '操作失败', icon: 'none' }); }
}}>
<Text className='ai-feedback-btn-text'></Text>
</View>
<View className='ai-feedback-btn ai-feedback-ignore' onClick={async () => {
try {
await submitSuggestionFeedback(s.id, 'ignore');
Taro.showToast({ title: '已忽略', icon: 'success' });
fetchData();
} catch { Taro.showToast({ title: '操作失败', icon: 'none' }); }
}}>
<Text className='ai-feedback-btn-text'></Text>
</View>
<View className='ai-feedback-btn ai-feedback-consult' onClick={async () => {
try {
await submitSuggestionFeedback(s.id, 'consult');
safeNavigateTo('/pages/consultation/index');
} catch { Taro.showToast({ title: '操作失败', icon: 'none' }); }
}}>
<Text className='ai-feedback-btn-text'></Text>
</View>
</View>
</View>
);
})}

View File

@@ -41,6 +41,17 @@ export async function listPendingSuggestions() {
return resp.data || [];
}
export async function submitSuggestionFeedback(
suggestionId: string,
action: 'adopt' | 'ignore' | 'consult',
feedbackText?: string,
) {
return api.post(`/ai/suggestions/${suggestionId}/feedback`, {
action,
feedback_text: feedbackText || null,
});
}
// === 健康摘要 ===
export interface SummaryItem {