feat(appointment): 新增 StepIndicator 步骤指示器 + WeekCalendar 周视图日历组件
This commit is contained in:
71
apps/miniprogram/src/components/StepIndicator/index.scss
Normal file
71
apps/miniprogram/src/components/StepIndicator/index.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
apps/miniprogram/src/components/StepIndicator/index.tsx
Normal file
42
apps/miniprogram/src/components/StepIndicator/index.tsx
Normal 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'>✓</Text> : <Text className='step-num'>{idx + 1}</Text>}
|
||||||
|
</View>
|
||||||
|
<Text className={`step-label ${isCurrent ? 'step-current' : ''} ${isDone ? 'step-done' : ''}`}>
|
||||||
|
{step.label}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
apps/miniprogram/src/components/WeekCalendar/index.scss
Normal file
84
apps/miniprogram/src/components/WeekCalendar/index.scss
Normal 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;
|
||||||
|
}
|
||||||
63
apps/miniprogram/src/components/WeekCalendar/index.tsx
Normal file
63
apps/miniprogram/src/components/WeekCalendar/index.tsx
Normal 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)}>◀</Text>
|
||||||
|
<Text className='week-label'>{dates[0].slice(5)} ~ {dates[6].slice(5)}</Text>
|
||||||
|
<Text className='week-arrow' onClick={() => setWeekOffset(weekOffset + 1)}>▶</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user