Trong các ứng dụng cần theo dõi tiến độ (đơn hàng, dự án, lịch sử thao tác...), Timeline là thành phần quan trọng để thể hiện chuỗi sự kiện theo thời gian. Bài viết phân tích chi tiết cách triển khai Timeline đa dạng bố cục trong React Native for OpenHarmony.
Cấu trúc dữ liệu
interface EventNode {
title: string;
detail?: string;
timestamp?: string;
marker?: string;
statusColor?: string;
}
Mỗi mốc thời gian bao gồm:
title: Tiêu đề bắt buộc (Ví dụ: "Đơn hàng được tạo")detail: Mô tả bổ sung (Ví dụ: "Từ kho Hà Nội")timestamp: Thời điểm xảy ra (không bắt buộc)marker: Biểu tượng tùy chọn (Ví dụ: 📦, ✅)statusColor: Màu sắc đại diện trạng thái
Cấu hình Timeline
interface TimelineConfig {
nodes: EventNode[];
layoutType?: 'standard' | 'alternating' | 'right-aligned';
connectorColor?: string;
customStyle?: ViewStyle;
}
Khác biệt hóa trải nghiệm với 3 kiểu bố cục:
- standard: Nội dung bên phải, phù hợp đa số trường hợp
- alternating: Lượt sang trái-phải theo thứ tự chẵn-lẻ
- right-aligned: Toàn bộ nội dung bên trái
Triển khai chính
const TimelineComponent = ({
nodes,
layoutType = 'standard',
connectorColor = '#e5e7eb',
customStyle
}) => {
return (
<View style={customStyle}>
{nodes.map((node, index) => {
const isFinal = index === nodes.length - 1;
const markerColor = node.statusColor || '#3b82f6';
const rightAligned = layoutType === 'right-aligned' || (layoutType === 'alternating' && index % 2);
return (
<View key={index} style={[baseStyles.container, rightAligned && baseStyles.rightContainer]}>
{layoutType === 'alternating' && (
<View style={[baseStyles.sidePanel, rightAligned ? baseStyles.leftPanel : baseStyles.rightPanel]}>
{!rightAligned && node.timestamp && <Text style={baseStyles.time}>{node.timestamp}</Text>}
{rightAligned && (
<>
<Text style={baseStyles.header}>{node.title}</Text>
{node.detail && <Text style={baseStyles.subtext}>{node.detail}</Text>}
</>
)}
</View>
)}
<View style={baseStyles.connector}>
<View style={[baseStyles.marker, { backgroundColor: markerColor }]}>
{node.marker && <Text style={baseStyles.markerIcon}>{node.marker}</Text>}
</View>
{!isFinal && <View style={[baseStyles.line, { backgroundColor: connectorColor }]} />}
</View>
<View style={[baseStyles.content, layoutType === 'right-aligned' && baseStyles.rightContent]}>
{layoutType !== 'alternating' && node.timestamp && <Text style={baseStyles.time}>{node.timestamp}</Text>}
{(layoutType !== 'alternating' || !rightAligned) && (
<>
<Text style={baseStyles.header}>{node.title}</Text>
{node.detail && <Text style={baseStyles.subtext}>{node.detail}</Text>}
</>
)}
{layoutType === 'alternating' && rightAligned && node.timestamp && <Text style={baseStyles.time}>{node.timestamp}</Text>}
</View>
</View>
);
})}
</View>
);
};
Phân tích điểm mấu chốt
- Điều kiện render: Tính toán
rightAligneddựa trên kiểu bố cục và chỉ số phần tử - Đối xứng nội dung: Trong chế độ
alternating, tiêu đề và thời gian được chia về hai phía - Đường kết nối: Sử dụng
flex: 1để đường thẳng tự động điều chỉnh chiều dài - Biểu tượng trạng thái: Cho phép cá nhân hóa bằng thuộc tính
marker
Thiết kế giao diện
const baseStyles = StyleSheet.create({
container: { flexDirection: 'row', minHeight: 60 },
rightContainer: { flexDirection: 'row-reverse' },
sidePanel: { flex: 1, paddingHorizontal: 16 },
leftPanel: { alignItems: 'flex-end' },
rightPanel: { alignItems: 'flex-start' },
connector: { alignItems: 'center', width: 24 },
marker: {
width: 12,
height: 12,
borderRadius: 6,
alignItems: 'center',
justifyContent: 'center',
},
markerIcon: { fontSize: 8, color: '#fff' },
line: { flex: 1, width: 2, marginVertical: 4 },
content: { flex: 1, paddingLeft: 16, paddingBottom: 24 },
rightContent: { paddingLeft: 0, paddingRight: 16, alignItems: 'flex-end' },
time: { fontSize: 12, color: '#9ca3af', marginBottom: 2 },
header: { fontSize: 16, fontWeight: '500', color: '#1f2937' },
subtext: { fontSize: 14, color: '#4b5563', marginTop: 2 },
});
Ứng dụng thực tế
Quản lý đơn hàng
const OrderHistory = ({ trackingData }) => {
const timelineItems = trackingData.map(item => ({
title: item.status,
detail: item.location,
timestamp: formatDate(item.date),
statusColor: item.isCurrent ? '#3b82f6' : '#10b981'
}));
return <TimelineComponent nodes={timelineItems} layoutType="alternating" />;
};
Lịch sử thao tác
const ActivityLog = ({ logs }) => {
const eventNodes = logs.map(log => ({
title: `${log.user} - ${log.action}`,
detail: log.description,
timestamp: formatTime(log.datetime),
marker: getActionIcon(log.type)
}));
return <TimelineComponent nodes={eventNodes} connectorColor="#e5e7eb" />;
};
Lưu ý quan trọng
- Không hiển thị đường kết nối cuối cùng: Kiểm tra
isFinaltrước khi render - Màu sắc biểu tượng: Sử dụng hệ thống màu nhất quán để thể hiện trạng thái (xanh lá = hoàn thành, cam = chờ xử lý...)
- Độ cao tối thiểu: Đảm bảo các mục có nội dung ngắn vẫn có kích thước đồng đều
- Đối xứng trong chế độ alternating: Căn chỉnh
alignItemsđể nội dung sát vào đường kết nối