feat: implement user dashboard with room viewing and booking
This commit is contained in:
167
pages/user.vue
167
pages/user.vue
@ -1,9 +1,172 @@
|
||||
<template>
|
||||
<div class="p-8">
|
||||
<div class="container mx-auto p-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">用户中心</h1>
|
||||
<p class="mt-4">欢迎回来!</p>
|
||||
<div class="text-lg">
|
||||
<span id="user-id" class="font-mono bg-gray-200 px-2 py-1 rounded">用户ID: {{ customerId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="message" class="text-red-500 text-center mb-4">{{ message }}</div>
|
||||
|
||||
<div class="mb-4 border-b border-gray-200">
|
||||
<nav class="-mb-px flex space-x-8" aria-label="Tabs">
|
||||
<button @click="currentView = 'rooms'" :class="['whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm', currentView === 'rooms' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300']">
|
||||
浏览房间
|
||||
</button>
|
||||
<button @click="currentView = 'reservations'" :class="['whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm', currentView === 'reservations' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300']">
|
||||
我的预订
|
||||
</button>
|
||||
<button @click="currentView = 'book'" :class="['whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm', currentView === 'book' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300']">
|
||||
预订房间
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Rooms Section -->
|
||||
<div v-if="currentView === 'rooms'">
|
||||
<h2 class="text-xl font-semibold mb-4">可用房间列表</h2>
|
||||
<table class="min-w-full bg-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2 px-4 border-b">房间ID</th>
|
||||
<th class="py-2 px-4 border-b">类型</th>
|
||||
<th class="py-2 px-4 border-b">价格</th>
|
||||
<th class="py-2 px-4 border-b">特色</th>
|
||||
<th class="py-2 px-4 border-b">余量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="room in rooms" :key="room.RoomID">
|
||||
<td class="py-2 px-4 border-b text-center">{{ room.RoomID }}</td>
|
||||
<td class="py-2 px-4 border-b text-center">{{ room.Type }}</td>
|
||||
<td class="py-2 px-4 border-b text-center">{{ room.Price }}</td>
|
||||
<td class="py-2 px-4 border-b text-center">{{ room.Feature || '-' }}</td>
|
||||
<td class="py-2 px-4 border-b text-center">{{ room.AvailableCount }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Reservations Section -->
|
||||
<div v-if="currentView === 'reservations'">
|
||||
<h2 class="text-xl font-semibold mb-4">我的预订记录</h2>
|
||||
<table class="min-w-full bg-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2 px-4 border-b">预订ID</th>
|
||||
<th class="py-2 px-4 border-b">房间ID</th>
|
||||
<th class="py-2 px-4 border-b">入住时间</th>
|
||||
<th class="py-2 px-4 border-b">入住天数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="res in reservations" :key="res.ReservationID">
|
||||
<td class="py-2 px-4 border-b text-center">{{ res.ReservationID }}</td>
|
||||
<td class="py-2 px-4 border-b text-center">{{ res.RoomID }}</td>
|
||||
<td class="py-2 px-4 border-b text-center">{{ new Date(res.CheckInTime).toLocaleDateString() }}</td>
|
||||
<td class="py-2 px-4 border-b text-center">{{ res.StayDays }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Booking Form Section -->
|
||||
<div v-if="currentView === 'book'">
|
||||
<h2 class="text-xl font-semibold mb-4">预订房间</h2>
|
||||
<div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow">
|
||||
<form @submit.prevent="bookRoom">
|
||||
<div class="mb-4">
|
||||
<label for="book-room-id" class="block text-sm font-medium text-gray-700">房间ID</label>
|
||||
<input type="number" id="book-room-id" v-model.number="bookingForm.roomId" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="book-checkin" class="block text-sm font-medium text-gray-700">入住时间</label>
|
||||
<input type="date" id="book-checkin" v-model="bookingForm.checkInTime" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="book-staydays" class="block text-sm font-medium text-gray-700">入住天数</label>
|
||||
<input type="number" id="book-staydays" v-model.number="bookingForm.stayDays" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
立即预订
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
|
||||
const customerId = ref<string | null>(null);
|
||||
const message = ref('');
|
||||
const currentView = ref('rooms'); // rooms, reservations, book
|
||||
|
||||
const rooms = ref([]);
|
||||
const reservations = ref([]);
|
||||
const bookingForm = reactive({
|
||||
roomId: null,
|
||||
checkInTime: '',
|
||||
stayDays: null,
|
||||
});
|
||||
|
||||
async function viewRooms() {
|
||||
try {
|
||||
const data = await $fetch('/api/rooms');
|
||||
rooms.value = data;
|
||||
} catch (error: any) {
|
||||
message.value = error.data?.message || '加载房间失败';
|
||||
}
|
||||
}
|
||||
|
||||
async function viewReservations() {
|
||||
if (!customerId.value) return;
|
||||
try {
|
||||
const data = await $fetch(`/api/reservations/${customerId.value}`);
|
||||
reservations.value = data;
|
||||
} catch (error: any) {
|
||||
message.value = error.data?.message || '加载预订失败';
|
||||
}
|
||||
}
|
||||
|
||||
async function bookRoom() {
|
||||
if (!bookingForm.roomId || !bookingForm.checkInTime || !bookingForm.stayDays) {
|
||||
message.value = '请填写完整信息';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await $fetch('/api/book', {
|
||||
method: 'POST',
|
||||
body: { ...bookingForm, customerId: customerId.value }
|
||||
});
|
||||
message.value = response.message;
|
||||
bookingForm.roomId = null;
|
||||
bookingForm.checkInTime = '';
|
||||
bookingForm.stayDays = null;
|
||||
currentView.value = 'reservations'; // Switch to reservations view on success
|
||||
} catch (error: any) {
|
||||
message.value = error.data?.message || '预订失败';
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
customerId.value = localStorage.getItem('customerId');
|
||||
if (!customerId.value) {
|
||||
navigateTo('/'); // Redirect to login if no customerId
|
||||
} else {
|
||||
viewRooms();
|
||||
viewReservations();
|
||||
}
|
||||
});
|
||||
|
||||
watch(currentView, (newView) => {
|
||||
message.value = '';
|
||||
if (newView === 'rooms') {
|
||||
viewRooms();
|
||||
} else if (newView === 'reservations') {
|
||||
viewReservations();
|
||||
}
|
||||
});
|
||||
</script>
|
55
server/api/book.post.ts
Normal file
55
server/api/book.post.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { defineEventHandler, readBody, createError } from 'h3';
|
||||
import { db, rooms, reservations } from '~/server/db';
|
||||
import { eq, gt, and, sql } from 'drizzle-orm';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
const { customerId, roomId, checkInTime, stayDays } = body;
|
||||
|
||||
if (!customerId || !roomId || !checkInTime || !stayDays) {
|
||||
return createError({ statusCode: 400, statusMessage: '请填写完整信息' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// 1. Check for availability and get the room
|
||||
const room = await tx.query.rooms.findFirst({
|
||||
where: and(eq(rooms.id, roomId), gt(rooms.availableCount, 0)),
|
||||
columns: {
|
||||
id: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
// By throwing an error, we automatically roll back the transaction
|
||||
throw new Error('Room not available');
|
||||
}
|
||||
|
||||
// 2. Decrement available count
|
||||
await tx.update(rooms)
|
||||
.set({ availableCount: sql`${rooms.availableCount} - 1` })
|
||||
.where(eq(rooms.id, roomId));
|
||||
|
||||
// 3. Create reservation
|
||||
const newReservation = await tx.insert(reservations).values({
|
||||
customerId: parseInt(customerId, 10),
|
||||
roomId: parseInt(roomId, 10),
|
||||
checkInTime: new Date(checkInTime),
|
||||
stayDays: parseInt(stayDays, 10),
|
||||
}).returning({ id: reservations.id });
|
||||
|
||||
return newReservation[0];
|
||||
});
|
||||
|
||||
return {
|
||||
message: '预订成功!',
|
||||
reservationId: result.id,
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.message === 'Room not available') {
|
||||
return createError({ statusCode: 409, statusMessage: '该房间已无可预订数量' });
|
||||
}
|
||||
console.error('Booking error:', error);
|
||||
return createError({ statusCode: 500, statusMessage: '预订失败,请稍后重试' });
|
||||
}
|
||||
});
|
35
server/api/reservations/[customerId].get.ts
Normal file
35
server/api/reservations/[customerId].get.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { defineEventHandler, getRouterParam } from 'h3';
|
||||
import { db } from '~/server/db';
|
||||
import { reservations } from '~/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const customerId = getRouterParam(event, 'customerId');
|
||||
|
||||
if (!customerId) {
|
||||
return createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Customer ID is required',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const userReservations = await db
|
||||
.select({
|
||||
ReservationID: reservations.id,
|
||||
RoomID: reservations.roomId,
|
||||
CheckInTime: reservations.checkInTime,
|
||||
StayDays: reservations.stayDays,
|
||||
})
|
||||
.from(reservations)
|
||||
.where(eq(reservations.customerId, parseInt(customerId, 10)));
|
||||
|
||||
return userReservations;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching reservations for customer ${customerId}:`, error);
|
||||
return createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch reservations',
|
||||
});
|
||||
}
|
||||
});
|
28
server/api/rooms/index.get.ts
Normal file
28
server/api/rooms/index.get.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { defineEventHandler } from 'h3';
|
||||
import { db } from '~/server/db';
|
||||
import { rooms, roomTypes } from '~/server/db/schema';
|
||||
import { eq, gt } from 'drizzle-orm';
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
try {
|
||||
const availableRooms = await db
|
||||
.select({
|
||||
RoomID: rooms.id,
|
||||
Type: roomTypes.typeName,
|
||||
Price: rooms.price,
|
||||
Feature: rooms.feature,
|
||||
AvailableCount: rooms.availableCount,
|
||||
})
|
||||
.from(rooms)
|
||||
.leftJoin(roomTypes, eq(rooms.typeId, roomTypes.id))
|
||||
.where(gt(rooms.availableCount, 0));
|
||||
|
||||
return availableRooms;
|
||||
} catch (error) {
|
||||
console.error('Error fetching rooms:', error);
|
||||
return createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch rooms',
|
||||
});
|
||||
}
|
||||
});
|
Reference in New Issue
Block a user