fix(mp): 健康页滚动卡死 + 文章样式丢失 — ScrollView height:0 修复 + RichArticle 18 条 tag-style 规则

- 健康页:移除冗余"健康"标题栏,ScrollView scrollY 添加 height:0 修复 flex 高度分配
- 健康页:useReachBottom(页面级)替换为 ScrollView onScrollToLower,修复模拟器卡死
- RichArticle:新增 18 条 tag-style 规则(h1-h4/p/ul/ol/li/table/th/td 等),确保文章内容在小程序中正确渲染样式
This commit is contained in:
iven
2026-05-27 19:37:25 +08:00
parent 23cd0b14a7
commit ddf5c196e4
3 changed files with 248 additions and 702 deletions

View File

@@ -7,6 +7,28 @@ interface RichArticleProps {
className?: string;
}
const TAG_STYLE = JSON.stringify({
h1: 'font-size:20px;font-weight:700;color:#2D2A26;margin:16px 0 8px',
h2: 'font-size:18px;font-weight:700;color:#2D2A26;margin:16px 0 8px',
h3: 'font-size:16px;font-weight:700;color:#2D2A26;margin:16px 0 8px',
h4: 'font-size:15px;font-weight:600;color:#2D2A26;margin:12px 0 6px',
p: 'font-size:16px;color:#2D2A26;line-height:1.85;margin-bottom:12px',
ul: 'padding-left:20px;margin:8px 0;font-size:16px;line-height:1.9;color:#2D2A26',
ol: 'padding-left:20px;margin:8px 0;font-size:16px;line-height:1.9;color:#2D2A26',
li: 'margin-bottom:4px',
blockquote: 'border-left:3px solid #C4623A;padding:6px 12px;color:#5A554F;margin:12px 0',
strong: 'font-weight:700;color:#2D2A26',
em: 'font-style:italic',
code: 'background:#F5F0EB;padding:2px 6px;border-radius:4px;font-size:14px;color:#C4623A',
pre: 'background:#F5F0EB;padding:12px;border-radius:8px;margin:14px 0;overflow-x:auto',
table: 'width:100%;border-collapse:collapse;margin:8px 0;font-size:14px',
th: 'border:1px solid #E8E2DC;padding:6px 8px;background:#FAF8F5;font-weight:600;text-align:left',
td: 'border:1px solid #E8E2DC;padding:6px 8px',
hr: 'border:none;border-top:1px dashed #D1D5DB;margin:14px 0',
img: 'max-width:100%;border-radius:8px;margin:8px 0;display:block',
a: 'color:#C4623A;text-decoration:none',
});
function prepareHtml(raw: string): string {
return sanitizeHtml(raw);
}
@@ -23,7 +45,7 @@ function RichArticle({ html, className }: RichArticleProps) {
lazy-load
selectable
container-style="font-size:16px;color:#5A554F;line-height:1.8;word-break:break-word"
tag-style='{"img":"max-width:100%;border-radius:8px;margin:12px auto;display:block","a":"color:#C4623A;text-decoration:none"}'
tag-style={TAG_STYLE}
/>
</View>
);

View File

@@ -2,515 +2,131 @@
@import '../../styles/mixins.scss';
.health-page {
padding-bottom: calc(var(--tk-tabbar-space) + env(safe-area-inset-bottom));
}
/* ─── 页头 ─── */
.health-header {
margin-bottom: var(--tk-gap-sm);
display: flex;
align-items: baseline;
justify-content: space-between;
}
.health-title {
@include serif-number;
font-size: var(--tk-font-h1);
font-weight: 700;
color: $tx;
letter-spacing: -0.02em;
}
.health-date {
font-size: var(--tk-font-cap);
color: $tx3;
}
/* ─── 今日体征 hero 卡片 ─── */
.vitals-grid {
margin-bottom: var(--tk-section-gap);
background: linear-gradient(135deg, $card 60%, $pri-l);
border-radius: var(--tk-card-radius);
box-shadow: $shadow-md;
padding: var(--tk-card-padding);
/* 覆盖 ContentCard 默认 padding/margin */
&.content-card {
padding: var(--tk-card-padding);
margin-bottom: var(--tk-section-gap);
}
}
.vitals-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--tk-gap-md);
}
.vitals-title {
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: $tx2;
letter-spacing: 0.04em;
}
.vitals-badge {
font-size: var(--tk-font-micro);
color: $acc;
background: $acc-l;
padding: 3px 10px;
border-radius: $r-pill;
font-weight: 500;
}
.vitals-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--tk-gap-sm);
}
.vital-cell {
text-align: center;
padding: var(--tk-gap-md) var(--tk-gap-sm);
border-radius: $r-sm;
background: $bg;
&:active {
opacity: var(--tk-touch-feedback-opacity);
}
}
.vital-value {
@include serif-number;
font-size: var(--tk-font-num);
font-weight: 700;
color: $tx;
font-variant-numeric: tabular-nums;
display: block;
line-height: 1.1;
}
.vital-unit {
font-size: var(--tk-font-micro);
color: $tx3;
display: block;
margin-top: 2px;
}
.vital-label {
font-size: var(--tk-font-cap);
color: $tx2;
font-weight: 500;
display: block;
margin-top: 6px;
}
.vital-cell.vital-warn {
background: $wrn-l;
.vital-value {
color: $wrn;
}
}
.vital-cell.vital-ok {
.vital-value {
color: $acc;
}
}
/* ─── 快捷入口 — 横排 4 格图标 ─── */
.quick-entries {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--tk-gap-xs);
margin-bottom: var(--tk-section-gap);
}
.quick-entry {
display: flex;
flex-direction: column;
height: 100vh;
}
/* ─── 分类标签 ─── */
.health-categories {
white-space: nowrap;
padding: var(--tk-gap-xs) var(--tk-page-padding);
margin-bottom: var(--tk-gap-xs);
flex-shrink: 0;
}
.health-cat-tab {
display: inline-flex;
align-items: center;
gap: var(--tk-gap-xs);
min-height: var(--tk-touch-min);
justify-content: center;
padding: var(--tk-gap-sm) 0;
&:active {
opacity: var(--tk-touch-feedback-opacity);
}
}
.quick-icon {
width: 44px;
height: 44px;
border-radius: $r-sm;
@include flex-center;
}
.quick-icon-text {
font-size: 18px;
font-weight: 600;
}
.quick-icon--input {
background: $pri-l;
.quick-icon-text {
color: $pri;
}
}
.quick-icon--trend {
background: $doc-pri-l;
.quick-icon-text {
color: $doc-pri;
}
}
.quick-icon--report {
background: $acc-l;
.quick-icon-text {
color: $acc;
}
}
.quick-icon--med {
background: $wrn-l;
.quick-icon-text {
color: $wrn;
}
}
.quick-label {
font-size: var(--tk-font-cap);
color: $tx2;
font-weight: 500;
}
/* ─── 告警横幅 ─── */
.alert-hint {
display: flex;
align-items: center;
gap: var(--tk-gap-sm);
margin-bottom: var(--tk-section-gap);
background: $dan-l;
border-radius: $r-sm;
/* 覆盖 ContentCard 默认样式 */
&.content-card {
background: $dan-l;
box-shadow: none;
border: none;
}
&:active {
opacity: var(--tk-touch-feedback-opacity);
}
}
.alert-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: $dan;
flex-shrink: 0;
}
.alert-text {
flex: 1;
padding: 8px 18px;
margin-right: 8px;
font-size: var(--tk-font-body-sm);
font-weight: 500;
color: $dan;
}
.alert-arrow {
font-size: var(--tk-font-body);
color: $dan;
flex-shrink: 0;
opacity: 0.6;
}
/* ─── 趋势图 ─── */
.trend-section {
margin-bottom: var(--tk-gap-sm);
}
.section-title {
@include section-title;
}
.trend-empty {
text-align: center;
}
.trend-empty-text {
font-size: var(--tk-font-cap);
font-weight: 400;
color: $tx2;
background: $surface-alt;
border-radius: 20px;
transition: all 0.2s;
&--active {
background: var(--tk-pri);
color: $white;
font-weight: 600;
box-shadow: 0 2px 8px rgba(196, 98, 58, 0.2);
}
}
.trend-chart {
padding: var(--tk-gap-md);
}
.trend-bars {
display: flex;
align-items: flex-end;
height: 120px;
background: $bg;
border-radius: $r-sm;
padding: var(--tk-gap-sm) var(--tk-gap-xs);
gap: 0;
position: relative;
}
.trend-threshold-line {
position: absolute;
left: 8px;
right: 8px;
border-top: 1.5px dashed $wrn;
opacity: 0.5;
pointer-events: none;
}
.trend-threshold-label {
position: absolute;
right: 0;
top: -16px;
font-size: var(--tk-font-micro);
color: $wrn;
opacity: 0.7;
}
.trend-bar-col {
/* ─── 可滚动内容区 ─── */
.health-scroll {
flex: 1;
overflow: hidden;
/* 微信小程序 ScrollView scrollY 需要显式高度 */
height: 0; /* flex:1 + height:0 让 flex 布局正确分配剩余高度 */
}
/* ─── 文章列表 ─── */
.health-article-list {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
justify-content: flex-end;
}
.trend-bar {
width: 24px;
border-radius: $r-xs $r-xs 0 0;
min-height: 6px;
&.trend-bar-normal {
background: var(--tk-pri);
opacity: 0.75;
}
&.trend-bar-warn {
background: $wrn;
opacity: 0.85;
}
}
.trend-bar-label {
font-size: var(--tk-font-micro);
color: var(--tk-text-secondary);
margin-top: var(--tk-gap-2xs);
}
/* ─── BLE 设备卡片 ─── */
.device-section {
margin-bottom: var(--tk-gap-sm);
}
.device-card {
display: flex;
align-items: center;
gap: var(--tk-gap-sm);
padding: 0 var(--tk-page-padding) var(--tk-gap-lg);
&:active {
opacity: var(--tk-touch-feedback-opacity);
.content-card {
display: flex;
gap: 14px;
}
}
.device-icon {
width: 48px;
height: 48px;
border-radius: $r-sm;
background: var(--tk-pri-l);
@include flex-center;
flex-shrink: 0;
}
.device-icon-text {
font-size: var(--tk-font-body);
}
.device-info {
.health-article-body {
flex: 1;
display: flex;
min-width: 0;
}
.device-name {
font-size: var(--tk-font-cap);
font-weight: 500;
color: $tx;
display: block;
}
.device-desc {
font-size: var(--tk-font-cap);
color: $acc;
display: block;
}
.device-arrow {
font-size: var(--tk-font-cap);
color: var(--tk-text-secondary);
flex-shrink: 0;
}
/* ─── 健康资讯入口 ─── */
.article-entry {
&:active {
opacity: var(--tk-touch-feedback-opacity);
}
}
.article-entry-text {
font-size: var(--tk-font-body-sm);
color: $tx;
font-weight: 500;
}
/* ─── AI 建议卡片 ─── */
.ai-suggestion-card {
background: linear-gradient(135deg, #F0F7F0 0%, $acc-l 100%);
border-radius: $r;
padding: var(--tk-card-padding);
margin-bottom: var(--tk-section-gap);
box-shadow: none;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, $acc, $acc 60%, transparent);
border-radius: 3px 3px 0 0;
}
}
.ai-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--tk-gap-md);
}
.ai-card-title {
font-size: var(--tk-font-body-sm);
font-weight: 600;
color: $acc;
}
.ai-card-count {
font-size: var(--tk-font-micro);
color: $acc;
opacity: 0.7;
}
.ai-suggestion-item {
padding: var(--tk-gap-sm) 0;
border-bottom: 1px solid rgba($acc, 0.12);
&:last-child {
border-bottom: none;
}
}
.ai-suggestion-main {
display: flex;
align-items: flex-start;
gap: var(--tk-gap-xs);
&:active {
opacity: var(--tk-touch-feedback-opacity);
}
}
.ai-risk-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 5px;
&.ai-risk-high {
background: $dan;
}
&.ai-risk-medium {
background: $wrn;
}
&.ai-risk-low {
background: $acc;
}
}
.ai-suggestion-text {
font-size: var(--tk-font-cap);
color: $tx2;
line-height: 1.6;
.health-article-content {
flex: 1;
}
/* ─── AI 建议反馈按钮 ─── */
.ai-feedback-row {
display: flex;
gap: var(--tk-gap-xs);
margin-top: var(--tk-gap-xs);
padding-left: 20px;
flex-direction: column;
justify-content: space-between;
min-width: 0;
}
.ai-feedback-btn {
height: 36px;
min-height: 36px;
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);
}
.health-article-title {
font-family: Georgia, 'Times New Roman', serif;
font-size: var(--tk-font-body);
font-weight: 700;
color: $tx;
line-height: 1.35;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.ai-feedback-btn-text {
font-size: var(--tk-font-micro);
font-weight: 500;
.health-article-summary {
font-size: var(--tk-font-cap);
color: $tx2;
line-height: 1.4;
display: block;
margin-bottom: var(--tk-gap-2xs);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-feedback-adopt .ai-feedback-btn-text {
color: $acc;
.health-article-meta {
display: flex;
gap: var(--tk-gap-sm);
font-size: var(--tk-font-micro);
color: $tx3;
align-items: center;
}
.ai-feedback-consult .ai-feedback-btn-text {
.health-article-tag {
font-size: var(--tk-font-micro);
color: var(--tk-pri);
background: var(--tk-pri-l);
padding: 2px 8px;
border-radius: $r-xs;
}
.health-article-date {
font-size: var(--tk-font-micro);
color: $tx3;
}
// 长者模式
.elder-mode .health-page {
.health-cat-tab {
padding: 10px 20px;
font-size: 15px;
}
.health-article-title {
font-size: 18px;
}
.health-article-summary {
font-size: 15px;
}
}

View File

@@ -1,244 +1,152 @@
import { View, Text } from '@tarojs/components';
import { useState, useCallback } from 'react';
import { View, Text, ScrollView } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { safeNavigateTo } from '@/utils/navigate';
import { usePageData } from '@/hooks/usePageData';
import { useAuthStore } from '../../stores/auth';
import { useElderClass } from '../../hooks/useElderClass';
import GuestGuard from '../../components/GuestGuard';
import Loading from '../../components/Loading';
import {
listArticles,
listCategories,
listPublicArticles,
listPublicCategories,
type Article,
type ArticleCategory,
} from '../../services/article';
import PageShell from '@/components/ui/PageShell';
import ContentCard from '@/components/ui/ContentCard';
import SegmentTabs from '../../components/SegmentTabs';
import { useHealthOverview, VITAL_TABS, type VitalType } from './useHealthOverview';
import { submitSuggestionFeedback } from '../../services/ai-analysis';
import EmptyState from '../../components/EmptyState';
import ErrorState from '../../components/ErrorState';
import Loading from '../../components/Loading';
import { useElderClass } from '../../hooks/useElderClass';
import './index.scss';
const QUICK_ENTRIES = [
{ label: '录入体征', icon: '✏', color: 'input', path: '/pages/pkg-health/input/index' },
{ label: '健康趋势', icon: '📈', color: 'trend', path: '/pages/pkg-health/trend/index' },
{ label: '我的报告', icon: '📋', color: 'report', path: '/pages/pkg-profile/reports/index' },
{ label: '健康档案', icon: '健', color: 'med', path: '/pages/pkg-profile/health-records/index' },
] as const;
function statusClass(status?: string): string {
if (!status) return '';
if (status === 'high' || status === 'abnormal') return 'vital-warn';
if (status === 'low') return 'vital-warn';
return 'vital-ok';
}
function formatDate(): string {
const d = new Date();
const month = d.getMonth() + 1;
const day = d.getDate();
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
return `${month}${day}日 周${weekDays[d.getDay()]}`;
}
export default function Health() {
const user = useAuthStore((s) => s.user);
const modeClass = useElderClass();
const {
todaySummary, loading, error, activeTab, trendData, trendLoading,
aiSuggestions, thresholds, alertCount, handleTabChange, fetchData,
} = useHealthOverview();
const isLoggedIn = !!useAuthStore((s) => s.user);
const [articles, setArticles] = useState<Article[]>([]);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [categories, setCategories] = useState<ArticleCategory[]>([]);
const [activeCategory, setActiveCategory] = useState<string | null>(null);
if (!user) {
return <GuestGuard title='请先登录' desc='登录后即可查看健康数据' />;
}
if (error) {
return (
<PageShell padding="md" safeBottom={false} scroll={false} className={`health-page ${modeClass}`}>
<View className='health-header'>
<Text className='health-title'></Text>
</View>
<Loading />
</PageShell>
);
}
const maxTrendValue = trendData.reduce((max, d) => Math.max(max, d.value), 1);
const dayLabels = ['日', '一', '二', '三', '四', '五', '六'];
const summary = todaySummary || {};
const vitals = [
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status },
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status },
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status },
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status },
];
const recordedCount = vitals.filter((v) => v.value !== '—').length;
const getThresholdValue = (type: VitalType): number | null => {
if (!thresholds.length) return null;
const th = thresholds;
if (type === 'blood_pressure') {
const v = th.find((t) => t.indicator === 'systolic_bp' && t.level === 'high');
return v?.threshold_value ?? 140;
const fetchData = useCallback(async (p: number, append = false, categoryId?: string | null) => {
setLoading(true);
setError(false);
try {
const cid = categoryId !== undefined ? categoryId : activeCategory;
const res = isLoggedIn
? await listArticles({ page: p, category_id: cid || undefined })
: await listPublicArticles({ page: p, category_id: cid || undefined });
const list = res.data || [];
setArticles(append ? (prev) => [...prev, ...list] : list);
setTotal(res.total);
setPage(p);
} catch (err) {
console.warn('[health] 加载文章列表失败:', err);
setError(true);
Taro.showToast({ title: '加载失败', icon: 'none' });
} finally {
setLoading(false);
}
if (type === 'heart_rate') {
const v = th.find((t) => t.indicator === 'heart_rate' && t.level === 'high');
return v?.threshold_value ?? 100;
}, [activeCategory, isLoggedIn]);
usePageData(
useCallback(async () => {
try {
const cats = isLoggedIn
? await listCategories()
: await listPublicCategories();
setCategories(cats || []);
} catch (err) {
console.warn('[health] 加载分类失败:', err);
setCategories([]);
}
await fetchData(1);
}, [fetchData, isLoggedIn]),
{ throttleMs: 10000, enablePullDown: true },
);
const loadMore = useCallback(() => {
if (!loading && articles.length < total) {
fetchData(page + 1, true);
}
if (type === 'blood_sugar') {
const v = th.find((t) => t.indicator === 'blood_sugar_fasting' && t.level === 'high');
return v?.threshold_value ?? 6.1;
}
return null;
}, [loading, articles.length, total, page, fetchData]);
const handleCategoryChange = (categoryId: string | null) => {
setActiveCategory(categoryId);
fetchData(1, false, categoryId);
};
const formatDate = (dateStr?: string) => {
if (!dateStr) return '';
const d = new Date(dateStr);
const month = d.getMonth() + 1;
const day = d.getDate();
return `${month}${day}`;
};
return (
<PageShell padding="md" safeBottom={false} scroll className={`health-page ${modeClass}`}>
<View className='health-header'>
<Text className='health-title'></Text>
<Text className='health-date'>{formatDate()}</Text>
</View>
<PageShell safeBottom={false} padding="none" scroll={false} className={`health-page ${modeClass}`}>
{/* 分类标签 */}
{categories.length > 0 && (
<ScrollView scrollX className='health-categories'>
<View
className={`health-cat-tab ${!activeCategory ? 'health-cat-tab--active' : ''}`}
onClick={() => handleCategoryChange(null)}
>
<Text></Text>
</View>
{categories.map((cat) => (
<View
key={cat.id}
className={`health-cat-tab ${activeCategory === cat.id ? 'health-cat-tab--active' : ''}`}
onClick={() => handleCategoryChange(cat.id)}
>
<Text>{cat.name}</Text>
</View>
))}
</ScrollView>
)}
{/* 今日体征 hero 卡片 */}
<View className='vitals-grid'>
<View className='vitals-header'>
<Text className='vitals-title'></Text>
{recordedCount > 0 && (
<Text className='vitals-badge'> {recordedCount} </Text>
)}
</View>
{loading ? <Loading /> : (
<View className='vitals-row'>
{vitals.map((v) => (
<View className={`vital-cell ${statusClass(v.status)}`} key={v.label}>
<Text className='vital-value'>{v.value}</Text>
<Text className='vital-unit'>{v.unit}</Text>
<Text className='vital-label'>{v.label}</Text>
</View>
{/* 文章列表 */}
<ScrollView scrollY className='health-scroll' onScrollToLower={loadMore} lowerThreshold={200}>
{error ? (
<ErrorState onRetry={() => fetchData(1, false, null)} />
) : articles.length === 0 && !loading ? (
<EmptyState text='暂无健康资讯' />
) : (
<View className='health-article-list'>
{articles.map((a) => (
<ContentCard
key={a.id}
padding='sm'
margin='none'
onPress={() => safeNavigateTo(`/pages/article/detail/index?id=${a.id}`)}
>
<View className='health-article-body'>
<View className='health-article-content'>
<Text className='health-article-title'>{a.title}</Text>
{a.summary && (
<Text className='health-article-summary'>{a.summary}</Text>
)}
<View className='health-article-meta'>
{(a.category_name || a.category) && (
<Text className='health-article-tag'>{a.category_name || a.category}</Text>
)}
{a.published_at && (
<Text className='health-article-date'>{formatDate(a.published_at)}</Text>
)}
</View>
</View>
</View>
</ContentCard>
))}
</View>
)}
</View>
{/* 快捷入口 — 横排 4 格图标 */}
<View className='quick-entries'>
{QUICK_ENTRIES.map((e) => (
<View
key={e.label}
className='quick-entry'
onClick={() => safeNavigateTo(e.path)}
>
<View className={`quick-icon quick-icon--${e.color}`}>
<Text className='quick-icon-text'>{e.icon}</Text>
</View>
<Text className='quick-label'>{e.label}</Text>
</View>
))}
</View>
{/* 告警横幅 */}
{alertCount > 0 && (
<ContentCard
variant="default"
padding="sm"
margin="none"
className='alert-hint'
onPress={() => safeNavigateTo('/pages/pkg-health/alerts/index')}
>
<View className='alert-dot' />
<Text className='alert-text'>{alertCount} </Text>
<Text className='alert-arrow'></Text>
</ContentCard>
)}
{/* AI 建议 */}
{aiSuggestions.length > 0 && (
<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>
</View>
{aiSuggestions.map((s) => {
const riskCls = s.risk_level === 'high' ? 'ai-risk-high' : s.risk_level === 'medium' ? 'ai-risk-medium' : 'ai-risk-low';
const params = s.params as Record<string, unknown> | null;
const reason = (params?.reason as string) || (params?.message as string) || '健康建议';
return (
<View key={s.id} className='ai-suggestion-item'>
<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, 50)}</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>
)}
{/* 7天趋势 */}
<View className='trend-section'>
<Text className='section-title'> 7 </Text>
<SegmentTabs tabs={VITAL_TABS} activeKey={activeTab} onChange={handleTabChange} variant="pill" />
{trendLoading ? <Loading /> : trendData.length === 0 ? (
<ContentCard padding="md">
<Text className='trend-empty-text'></Text>
</ContentCard>
) : (
<ContentCard padding="md">
<View className='trend-bars'>
{(() => {
const tv = getThresholdValue(activeTab);
if (tv) {
const pct = Math.min(95, (tv / maxTrendValue) * 100);
return (
<View className='trend-threshold-line' style={`bottom:${((12 + pct * 1.08) / 120 * 100).toFixed(1)}%;`}>
<Text className='trend-threshold-label'>{tv}</Text>
</View>
);
}
return null;
})()}
{trendData.map((point, i) => {
const heightPct = Math.max(8, (point.value / maxTrendValue) * 100);
const tv = getThresholdValue(activeTab);
const isAbnormal = tv ? point.value >= tv : false;
const dayOfWeek = new Date(point.date).getDay();
return (
<View className='trend-bar-col' key={i}>
<View
className={`trend-bar ${isAbnormal ? 'trend-bar-warn' : 'trend-bar-normal'}`}
style={`height:${heightPct}%;`}
/>
<Text className='trend-bar-label'>{dayLabels[dayOfWeek]}</Text>
</View>
);
})}
</View>
</ContentCard>
)}
</View>
{/* 健康资讯入口 */}
<ContentCard onPress={() => safeNavigateTo('/pages/article/index')}>
<Text className='article-entry-text'> </Text>
</ContentCard>
{loading && <Loading />}
</ScrollView>
</PageShell>
);
}