feat(health+miniprogram): 预约/报告/随访/资讯/家庭管理 — Chunk 4-6
后端: - 添加 articles 表迁移 + Entity + Service + Handler - 健康数据趋势 API (get_mini_trend) 注册路由 - article CRUD (list/get) + DTO 前端 (11个新页面 + 5个服务): - 预约挂号: 列表/创建向导/详情页 - 报告管理: 列表/详情页 - 随访管理: 任务列表/记录详情页 - 资讯文章: 文章详情页 - 个人中心: 就诊人管理/新增/我的报告/我的随访/用药提醒/设置 - 更新 app.config.ts 注册全部路由 - 更新 profile/article 页面为真实功能
This commit is contained in:
118
apps/miniprogram/src/pages/followup/detail/index.scss
Normal file
118
apps/miniprogram/src/pages/followup/detail/index.scss
Normal file
@@ -0,0 +1,118 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
padding: 24px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 28px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid $bd-l;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 26px;
|
||||
color: $tx2;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 26px;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
margin-top: 16px;
|
||||
padding: 20px;
|
||||
background: $bd-l;
|
||||
border-radius: $r-sm;
|
||||
}
|
||||
|
||||
.detail-desc-text {
|
||||
font-size: 26px;
|
||||
color: $tx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.submit-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 28px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.submit-textarea {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
background: $bd-l;
|
||||
border-radius: $r-sm;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: $pri;
|
||||
border-radius: $r-sm;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn-text {
|
||||
font-size: 30px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 120px 0;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
font-size: 28px;
|
||||
color: $tx3;
|
||||
}
|
||||
119
apps/miniprogram/src/pages/followup/detail/index.tsx
Normal file
119
apps/miniprogram/src/pages/followup/detail/index.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import { listTasks, submitRecord, FollowUpTask } from '../../../services/followup';
|
||||
import './index.scss';
|
||||
|
||||
export default function FollowUpDetail() {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
listTasks()
|
||||
.then((res) => {
|
||||
const found = (res.data || []).find((t) => t.id === id);
|
||||
setTask(found || null);
|
||||
})
|
||||
.catch(() => Taro.showToast({ title: '加载失败', icon: 'none' }))
|
||||
.finally(() => setLoading(false));
|
||||
}, [id]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!content.trim()) {
|
||||
Taro.showToast({ title: '请输入内容', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await submitRecord({
|
||||
task_id: id,
|
||||
content: { text: content.trim() },
|
||||
});
|
||||
Taro.showToast({ title: '提交成功', icon: 'success' });
|
||||
setContent('');
|
||||
} catch {
|
||||
Taro.showToast({ title: '提交失败', icon: 'none' });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
if (status === 'completed') return '已完成';
|
||||
if (status === 'overdue') return '已过期';
|
||||
return '待完成';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className='detail-page'>
|
||||
<View className='loading-state'>
|
||||
<Text className='loading-text'>加载中...</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) {
|
||||
return (
|
||||
<View className='detail-page'>
|
||||
<View className='empty-state'>
|
||||
<Text className='empty-text'>任务不存在</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const isCompleted = task.status === 'completed';
|
||||
|
||||
return (
|
||||
<View className='detail-page'>
|
||||
{/* 任务详情 */}
|
||||
<View className='detail-card'>
|
||||
<Text className='detail-title'>{task.task_type}</Text>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>状态</Text>
|
||||
<Text className='detail-value'>{getStatusLabel(task.status)}</Text>
|
||||
</View>
|
||||
<View className='detail-row'>
|
||||
<Text className='detail-label'>截止日期</Text>
|
||||
<Text className='detail-value'>{task.due_date}</Text>
|
||||
</View>
|
||||
{task.description && (
|
||||
<View className='detail-desc'>
|
||||
<Text className='detail-desc-text'>{task.description}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 提交表单 */}
|
||||
{!isCompleted && (
|
||||
<View className='submit-card'>
|
||||
<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>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
132
apps/miniprogram/src/pages/followup/index.scss
Normal file
132
apps/miniprogram/src/pages/followup/index.scss
Normal file
@@ -0,0 +1,132 @@
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.followup-page {
|
||||
min-height: 100vh;
|
||||
background: $bg;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background: $card;
|
||||
padding: 0;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px 0;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48px;
|
||||
height: 4px;
|
||||
background: $pri;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 28px;
|
||||
color: $tx2;
|
||||
|
||||
.tab-item.active & {
|
||||
color: $pri;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.task-list {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 28px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.task-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.task-status {
|
||||
font-size: 24px;
|
||||
padding: 4px 16px;
|
||||
border-radius: 20px;
|
||||
|
||||
&.pending {
|
||||
color: $wrn;
|
||||
background: $wrn-l;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
color: $acc;
|
||||
background: $acc-l;
|
||||
}
|
||||
|
||||
&.overdue {
|
||||
color: $dan;
|
||||
background: $dan-l;
|
||||
}
|
||||
}
|
||||
|
||||
.task-desc {
|
||||
font-size: 26px;
|
||||
color: $tx2;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-due {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 120px 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
.loading-hint {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
}
|
||||
107
apps/miniprogram/src/pages/followup/index.tsx
Normal file
107
apps/miniprogram/src/pages/followup/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
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 './index.scss';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'pending', label: '待完成' },
|
||||
{ key: 'completed', label: '已完成' },
|
||||
{ key: 'overdue', label: '已过期' },
|
||||
];
|
||||
|
||||
export default function FollowUpList() {
|
||||
const [activeTab, setActiveTab] = useState('pending');
|
||||
const [tasks, setTasks] = useState<FollowUpTask[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchTasks = useCallback(async (status: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await listTasks(status);
|
||||
setTasks(res.data || []);
|
||||
} catch {
|
||||
Taro.showToast({ title: '加载失败', icon: 'none' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useDidShow(() => {
|
||||
fetchTasks(activeTab);
|
||||
}, [activeTab, fetchTasks]);
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
setActiveTab(key);
|
||||
setTasks([]);
|
||||
fetchTasks(key);
|
||||
};
|
||||
|
||||
const goToDetail = (id: string) => {
|
||||
Taro.navigateTo({ url: `/pages/followup/detail/index?id=${id}` });
|
||||
};
|
||||
|
||||
const getStatusClass = (status: string) => {
|
||||
if (status === 'completed') return 'completed';
|
||||
if (status === 'overdue') return 'overdue';
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
if (status === 'completed') return '已完成';
|
||||
if (status === 'overdue') return '已过期';
|
||||
return '待完成';
|
||||
};
|
||||
|
||||
return (
|
||||
<View className='followup-page'>
|
||||
{/* Tab 栏 */}
|
||||
<View className='tab-bar'>
|
||||
{TABS.map((tab) => (
|
||||
<View
|
||||
className={`tab-item ${activeTab === tab.key ? 'active' : ''}`}
|
||||
key={tab.key}
|
||||
onClick={() => handleTabChange(tab.key)}
|
||||
>
|
||||
<Text className='tab-text'>{tab.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 任务列表 */}
|
||||
<View className='task-list'>
|
||||
{tasks.map((t) => (
|
||||
<View
|
||||
className='task-card'
|
||||
key={t.id}
|
||||
onClick={() => goToDetail(t.id)}
|
||||
>
|
||||
<View className='task-top'>
|
||||
<Text className='task-name'>{t.task_type}</Text>
|
||||
<Text className={`task-status ${getStatusClass(t.status)}`}>
|
||||
{getStatusLabel(t.status)}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className='task-desc'>{t.description}</Text>
|
||||
<Text className='task-due'>截止日期:{t.due_date}</Text>
|
||||
</View>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<View className='loading-hint'>
|
||||
<Text className='loading-text'>加载中...</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user