feat(appointment): 新增 StepIndicator 步骤指示器 + WeekCalendar 周视图日历组件

This commit is contained in:
iven
2026-04-24 12:40:59 +08:00
parent 4f2efdb643
commit 487432b4e9
4 changed files with 260 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
@import '../../styles/variables.scss';
.step-indicator {
display: flex;
align-items: flex-start;
justify-content: center;
padding: 24px 32px;
background: $card;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
position: relative;
}
.step-line {
position: absolute;
top: 24px;
left: -50%;
right: 50%;
height: 4px;
background: $bd-l;
transition: background 0.3s ease;
&.step-line-done {
background: $acc;
}
}
.step-dot {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: $bd-l;
color: $tx3;
font-size: 24px;
transition: all 0.3s ease;
z-index: 1;
&.step-current {
background: $pri;
color: white;
}
&.step-done {
background: $acc;
color: white;
}
}
.step-label {
font-size: 22px;
color: $tx3;
margin-top: 8px;
text-align: center;
&.step-current {
color: $pri;
font-weight: bold;
}
&.step-done {
color: $acc;
}
}

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { View, Text } from '@tarojs/components';
import './index.scss';
interface Step {
label: string;
}
interface StepIndicatorProps {
steps: Step[];
current: number;
onChange?: (index: number) => void;
}
export default function StepIndicator({ steps, current, onChange }: StepIndicatorProps) {
return (
<View className='step-indicator'>
{steps.map((step, idx) => {
const isCurrent = idx === current;
const isDone = idx < current;
const isClickable = isDone && !!onChange;
return (
<View className='step-item' key={step.label}>
{idx > 0 && (
<View className={`step-line ${isDone ? 'step-line-done' : ''}`} />
)}
<View
className={`step-dot ${isCurrent ? 'step-current' : ''} ${isDone ? 'step-done' : ''}`}
onClick={isClickable ? () => onChange(idx) : undefined}
>
{isDone ? <Text className='step-check'>&#10003;</Text> : <Text className='step-num'>{idx + 1}</Text>}
</View>
<Text className={`step-label ${isCurrent ? 'step-current' : ''} ${isDone ? 'step-done' : ''}`}>
{step.label}
</Text>
</View>
);
})}
</View>
);
}

View File

@@ -0,0 +1,84 @@
@import '../../styles/variables.scss';
.week-calendar {
background: $card;
border-radius: $r;
padding: 16px;
}
.week-nav {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.week-arrow {
font-size: 28px;
color: $pri;
padding: 0 16px;
}
.week-label {
font-size: 24px;
color: $tx;
font-weight: bold;
}
.week-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
text-align: center;
}
.week-cell {
padding: 8px 4px;
border-radius: $r-sm;
position: relative;
}
.cell-weekday {
font-size: 20px;
color: $tx3;
display: block;
}
.cell-date {
font-size: 26px;
color: $tx;
display: block;
margin-top: 4px;
}
.cell-today {
color: $pri;
font-weight: bold;
}
.cell-dot {
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
width: 8px;
height: 8px;
background: $acc;
border-radius: 50%;
}
.cell-selected {
background: $pri;
border-radius: $r-sm;
.cell-date { color: white; }
.cell-dot { background: white; }
}
.cell-empty .cell-date {
color: $bd;
}
.cell-past {
opacity: 0.4;
}

View File

@@ -0,0 +1,63 @@
import React, { useState } from 'react';
import { View, Text } from '@tarojs/components';
import './index.scss';
interface WeekCalendarProps {
scheduledDates: Set<string>;
selectedDate: string;
onSelectDate: (date: string) => void;
}
function getWeekDates(offset: number): string[] {
const now = new Date();
const monday = new Date(now);
monday.setDate(now.getDate() - now.getDay() + 1 + offset * 7);
const dates: string[] = [];
for (let i = 0; i < 7; i++) {
const d = new Date(monday);
d.setDate(monday.getDate() + i);
dates.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
}
return dates;
}
const WEEKDAYS = ['一', '二', '三', '四', '五', '六', '日'];
export default function WeekCalendar({ scheduledDates, selectedDate, onSelectDate }: WeekCalendarProps) {
const [weekOffset, setWeekOffset] = useState(0);
const dates = getWeekDates(weekOffset);
const today = (() => {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
})();
return (
<View className='week-calendar'>
<View className='week-nav'>
<Text className='week-arrow' onClick={() => setWeekOffset(weekOffset - 1)}>&#9664;</Text>
<Text className='week-label'>{dates[0].slice(5)} ~ {dates[6].slice(5)}</Text>
<Text className='week-arrow' onClick={() => setWeekOffset(weekOffset + 1)}>&#9654;</Text>
</View>
<View className='week-grid'>
{WEEKDAYS.map((day, idx) => {
const dateStr = dates[idx];
const isScheduled = scheduledDates.has(dateStr);
const isSelected = dateStr === selectedDate;
const isToday = dateStr === today;
const isPast = dateStr < today;
return (
<View
className={`week-cell ${isSelected ? 'cell-selected' : ''} ${!isScheduled ? 'cell-empty' : ''} ${isPast ? 'cell-past' : ''}`}
key={dateStr}
onClick={isScheduled && !isPast ? () => onSelectDate(dateStr) : undefined}
>
<Text className='cell-weekday'>{day}</Text>
<Text className={`cell-date ${isToday ? 'cell-today' : ''}`}>{parseInt(dateStr.slice(8))}</Text>
{isScheduled && <View className='cell-dot' />}
</View>
);
})}
</View>
</View>
);
}