- 新增 POST /ai/chat 端点,由 LLM(Ollama qwen3)担任 24h 健康客服"小华" - 新增 ai.chat.send 权限,绑定管理员/患者/医生/护士/健康管理师角色 - 消息页从咨询列表重构为单窗口 AI 对话(欢迎态 + 聊天态 + 快捷问诊) - 通知功能迁移到"我的"页面菜单项(带未读角标),独立通知列表页 - 修复气泡文字截断:改用百分比 max-width + block Text + pre-wrap 换行 - 修复权限绑定:迁移 SQL 角色名从英文改为中文(admin→管理员,patient→患者)
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import { View, Text } from '@tarojs/components';
|
|
import Taro, { useReachBottom } from '@tarojs/taro';
|
|
import { notificationService } from '@/services/notification';
|
|
import PageShell from '@/components/ui/PageShell';
|
|
import ContentCard from '@/components/ui/ContentCard';
|
|
import EmptyState from '@/components/EmptyState';
|
|
import ErrorState from '@/components/ErrorState';
|
|
import Loading from '@/components/Loading';
|
|
import { useElderClass } from '@/hooks/useElderClass';
|
|
import { usePageData } from '@/hooks/usePageData';
|
|
import './index.scss';
|
|
|
|
interface NotificationItem {
|
|
id: string;
|
|
title: string;
|
|
desc: string;
|
|
time: string;
|
|
type: string;
|
|
read?: boolean;
|
|
}
|
|
|
|
const TYPE_ICONS: Record<string, { icon: string; cls: string }> = {
|
|
appointment: { icon: '约', cls: 'ntype-appointment' },
|
|
alert: { icon: '警', cls: 'ntype-alert' },
|
|
followup: { icon: '随', cls: 'ntype-followup' },
|
|
points: { icon: '分', cls: 'ntype-points' },
|
|
report: { icon: '报', cls: 'ntype-report' },
|
|
};
|
|
|
|
export default function Notifications() {
|
|
const modeClass = useElderClass();
|
|
const [items, setItems] = useState<NotificationItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [page, setPage] = useState(1);
|
|
const [total, setTotal] = useState(0);
|
|
|
|
const load = useCallback(async (pageNum: number, isRefresh = false) => {
|
|
if (isRefresh) setLoading(true);
|
|
setError('');
|
|
try {
|
|
const res = await notificationService.list<{ data: NotificationItem[]; total?: number }>({
|
|
page: pageNum,
|
|
page_size: 20,
|
|
});
|
|
const list = res.data || [];
|
|
setItems((prev) => (isRefresh ? list : [...prev, ...list]));
|
|
setTotal(res.total || 0);
|
|
setPage(pageNum);
|
|
} catch {
|
|
setError('加载失败');
|
|
if (isRefresh) setItems([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
usePageData(
|
|
useCallback(async () => {
|
|
Taro.setNavigationBarTitle({ title: '消息通知' });
|
|
await load(1, true);
|
|
}, [load]),
|
|
{ throttleMs: 10000, enablePullDown: true },
|
|
);
|
|
|
|
useReachBottom(() => {
|
|
if (!loading && items.length < total) load(page + 1);
|
|
});
|
|
|
|
if (loading && items.length === 0) return <Loading />;
|
|
|
|
if (error && items.length === 0) {
|
|
return (
|
|
<PageShell className={modeClass}>
|
|
<ErrorState text={error} onRetry={() => load(1, true)} />
|
|
</PageShell>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageShell className={modeClass}>
|
|
{items.length === 0 ? (
|
|
<EmptyState text='暂无新通知' />
|
|
) : (
|
|
<View className='notify-list'>
|
|
{items.map((n) => {
|
|
const cfg = TYPE_ICONS[n.type] || TYPE_ICONS.report;
|
|
const unread = !n.read;
|
|
return (
|
|
<ContentCard
|
|
key={n.id}
|
|
padding='sm'
|
|
activeFeedback='none'
|
|
className={unread ? '' : 'notify-muted'}
|
|
onPress={async () => {
|
|
if (unread) {
|
|
try { await notificationService.markRead(n.id); } catch { /* ignore */ }
|
|
setItems((prev) => prev.map((x) => (x.id === n.id ? { ...x, read: true } : x)));
|
|
}
|
|
}}
|
|
>
|
|
<View className='notify-item'>
|
|
<View className={`notify-icon ${cfg.cls}`}>
|
|
<Text className='notify-icon-char'>{cfg.icon}</Text>
|
|
</View>
|
|
<View className='notify-body'>
|
|
<View className='notify-row'>
|
|
<Text className={`notify-title ${unread ? 'notify-bold' : ''}`}>{n.title}</Text>
|
|
<Text className='notify-time'>{n.time}</Text>
|
|
</View>
|
|
<Text className='notify-desc'>{n.desc}</Text>
|
|
</View>
|
|
{unread && <View className='notify-dot' />}
|
|
</View>
|
|
</ContentCard>
|
|
);
|
|
})}
|
|
</View>
|
|
)}
|
|
</PageShell>
|
|
);
|
|
}
|