| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- <template>
- <div class="grooming-booking">
- <el-steps :active="step" finish-status="success" class="steps">
- <el-step title="选择套餐" />
- <el-step title="选择预约时间" />
- <el-step title="选择宠物" />
- <el-step title="确认提交" />
- </el-steps>
- <!-- Step 1: 选择套餐 -->
- <div v-if="step === 1" class="step-content">
- <h2>选择洗护套餐</h2>
- <div v-if="packages.length === 0" class="empty">
- <el-empty description="暂无可用套餐" />
- </div>
- <el-radio-group v-else v-model="selectedPackageId" class="package-list">
- <el-card
- v-for="pkg in packages"
- :key="pkg.id"
- :class="['package-card', { selected: selectedPackageId === pkg.id }]"
- @click="selectedPackageId = pkg.id"
- >
- <el-radio :value="pkg.id" class="package-radio">
- <strong>{{ pkg.name }}</strong>
- <p>{{ pkg.description }}</p>
- <p class="price">¥{{ pkg.price }}</p>
- </el-radio>
- </el-card>
- </el-radio-group>
- <div class="step-actions">
- <el-button type="primary" :disabled="!selectedPackageId" @click="step = 2">下一步</el-button>
- </div>
- </div>
- <!-- Step 2: 选择预约时间 -->
- <div v-if="step === 2" class="step-content">
- <h2>选择预约时间</h2>
- <el-form label-width="120px" class="time-form">
- <el-form-item label="选择日期">
- <el-date-picker v-model="selectedDate" type="date" placeholder="选择日期" />
- </el-form-item>
- <el-form-item label="选择时段">
- <el-radio-group v-model="selectedTimeSlot">
- <el-radio value="MORNING">上午 (9:00-12:00)</el-radio>
- <el-radio value="AFTERNOON">下午 (14:00-18:00)</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-form>
- <div class="step-actions">
- <el-button @click="step = 1">上一步</el-button>
- <el-button type="primary" :disabled="!selectedDate || !selectedTimeSlot" @click="step = 3">下一步</el-button>
- </div>
- </div>
- <!-- Step 3: 选择宠物 -->
- <div v-if="step === 3" class="step-content">
- <h2>选择宠物</h2>
- <PetSelector
- :pet-list="myPets"
- :selected="selectedPet"
- @update:selected="onPetSelected"
- />
- <div class="step-actions">
- <el-button @click="step = 2">上一步</el-button>
- <el-button type="primary" :disabled="!selectedPet" @click="step = 4">下一步</el-button>
- </div>
- </div>
- <!-- Step 4: 确认提交 -->
- <div v-if="step === 4" class="step-content">
- <h2>确认提交</h2>
- <el-card class="summary-card">
- <el-descriptions :column="2" border>
- <el-descriptions-item label="套餐">{{ selectedPackage?.name }}</el-descriptions-item>
- <el-descriptions-item label="价格">¥{{ selectedPackage?.price }}</el-descriptions-item>
- <el-descriptions-item label="日期">{{ formatDate(selectedDate) }}</el-descriptions-item>
- <el-descriptions-item label="时段">{{ timeSlotLabel }}</el-descriptions-item>
- <el-descriptions-item label="宠物">{{ selectedPet?.name }}</el-descriptions-item>
- </el-descriptions>
- </el-card>
- <div class="step-actions">
- <el-button @click="step = 3">上一步</el-button>
- <el-button type="primary" @click="handleSubmit">提交预约</el-button>
- </div>
- </div>
- <PaymentDialog
- v-model:visible="showPayment"
- :amount="depositAmount"
- :order-id="createdOrderId"
- @success="onPaymentSuccess"
- />
- </div>
- </template>
- <script setup lang="ts">
- import { computed, ref, onMounted } from 'vue'
- import { useRouter } from 'vue-router'
- import { ElMessage } from 'element-plus'
- import type { Pet, Package } from '@/type'
- import { getPackageController } from '@/api/package-controller'
- import { getPetController } from '@/api/pet-controller'
- import { getAppointmentController } from '@/api/appointment-controller'
- import type { AppointmentDto } from '@/api/models'
- import PetSelector from '@/components/PetSelector.vue'
- import PaymentDialog from '@/components/PaymentDialog.vue'
- const router = useRouter()
- const step = ref(1)
- // Step 1
- const packages = ref<Package[]>([])
- const selectedPackageId = ref('')
- const selectedPackage = computed(() => packages.value.find(p => p.id === selectedPackageId.value))
- // Step 2
- const selectedDate = ref<Date | null>(null)
- const selectedTimeSlot = ref('')
- const timeSlotLabel = computed(() => {
- const map: Record<string, string> = { MORNING: '上午', AFTERNOON: '下午' }
- return map[selectedTimeSlot.value] || ''
- })
- // Step 3
- const myPets = ref<Pet[]>([])
- const selectedPet = ref<Pet | null>(null)
- // Payment
- const showPayment = ref(false)
- const depositAmount = ref(0)
- const createdOrderId = ref('')
- const fetchPackages = async () => {
- try {
- const api = getPackageController()
- const res = await api.list4({ type: 'GROOMING' })
- if (res.code === 200 && res.data) {
- packages.value = res.data.map(p => ({
- id: String(p.id ?? ''),
- type: 'GROOMING' as const,
- name: p.name ?? '',
- price: p.price ?? 0,
- description: p.description,
- durationDays: p.duration ?? 0,
- isActive: true
- }))
- }
- } catch {
- // ignore
- }
- }
- const fetchPets = async () => {
- try {
- const api = getPetController()
- const res = await api.list()
- if (res.code === 200 && res.data) {
- myPets.value = res.data.map(p => ({
- id: String(p.id ?? ''),
- name: p.name ?? '',
- breed: p.breed ?? '',
- age: p.birthDate ? Math.floor((Date.now() - new Date(p.birthDate).getTime()) / 31557600000) : 0,
- weight: p.weight ?? 0,
- gender: (p.gender as Pet['gender']) ?? 'MALE',
- notes: p.remark
- }))
- }
- } catch {
- // ignore
- }
- }
- const formatDate = (d: Date | null): string =>
- d
- ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
- : ''
- const onPetSelected = (pet: Pet) => {
- selectedPet.value = pet
- }
- const handleSubmit = async () => {
- try {
- const api = getAppointmentController()
- const dto: AppointmentDto = {
- packageId: Number(selectedPackageId.value),
- petId: Number(selectedPet.value?.id),
- startDate: selectedDate.value?.toISOString().slice(0, 10)
- }
- const res = await api.create(dto)
- if (res.code === 200 && res.data) {
- ElMessage.success('预约提交成功')
- const pkg = selectedPackage.value
- depositAmount.value = pkg ? Math.round(pkg.price * 0.3) : 0
- createdOrderId.value = String(res.data.id ?? '')
- showPayment.value = true
- } else {
- ElMessage.error(res.message || '预约提交失败')
- }
- } catch (err: any) {
- ElMessage.error(err?.response?.data?.message || err?.message || err || '预约提交失败')
- }
- }
- const onPaymentSuccess = () => {
- ElMessage.success('支付成功')
- router.push('/orders')
- }
- onMounted(() => {
- fetchPackages()
- fetchPets()
- })
- </script>
- <style scoped>
- .grooming-booking {
- max-width: 800px;
- margin: 0 auto;
- padding: 40px 20px;
- }
- .steps {
- margin-bottom: 40px;
- }
- .step-content {
- text-align: center;
- }
- .step-actions {
- margin-top: 32px;
- display: flex;
- gap: 12px;
- justify-content: center;
- }
- .package-list {
- display: flex;
- flex-direction: column;
- gap: 12px;
- max-width: 500px;
- margin: 20px auto;
- }
- .package-card {
- cursor: pointer;
- transition: all 0.2s;
- text-align: left;
- }
- .package-card:hover {
- border-color: var(--el-color-primary);
- }
- .package-card.selected {
- border-color: var(--el-color-primary);
- background-color: rgba(227, 72, 137, 0.05);
- }
- .package-radio {
- width: 100%;
- }
- .price {
- color: var(--el-color-primary);
- font-weight: bold;
- }
- .time-form {
- max-width: 400px;
- margin: 20px auto;
- text-align: left;
- }
- .summary-card {
- max-width: 500px;
- margin: 20px auto;
- text-align: left;
- }
- .empty {
- margin: 40px 0;
- }
- </style>
|