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