feat: implement admin dashboard with full CRUD functionality

This commit is contained in:
2025-06-18 16:20:19 +08:00
parent 95ef4cd617
commit 0ca2d51669
6 changed files with 324 additions and 3 deletions

View File

@ -1,9 +1,191 @@
<template>
<div class="p-8">
<h1 class="text-2xl font-bold">后台管理</h1>
<p class="mt-4">欢迎, 管理员!</p>
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-6">后台管理</h1>
<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 v-for="tab in tabs" :key="tab.key" @click="currentView = tab.key" :class="['whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm', currentView === tab.key ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300']">
{{ tab.name }}
</button>
</nav>
</div>
<!-- Customers -->
<div v-if="currentView === 'customers'">
<h2 class="text-xl font-semibold mb-4">客户列表</h2>
<table class="min-w-full bg-white">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>联系方式</th>
</tr>
</thead>
<tbody>
<tr v-for="c in customers" :key="c.CustomerID">
<td class="border px-4 py-2 text-center">{{ c.CustomerID }}</td>
<td class="border px-4 py-2 text-center">{{ c.Name }}</td>
<td class="border px-4 py-2 text-center">{{ c.Contact }}</td>
</tr>
</tbody>
</table>
<button @click="showAddUser = !showAddUser" class="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
{{ showAddUser ? '取消添加' : '添加用户' }}
</button>
<!-- Add User Form -->
<div v-if="showAddUser" class="mt-4 max-w-md bg-gray-50 p-6 rounded-lg shadow">
<form @submit.prevent="addUser">
<!-- Form fields from registration page, simplified -->
<div class="mb-2"><input v-model="addUserForm.name" placeholder="姓名" class="w-full p-2 border rounded"></div>
<div class="mb-2"><input v-model="addUserForm.contact" placeholder="手机号" class="w-full p-2 border rounded"></div>
<div class="mb-2"><input v-model="addUserForm.idCard" placeholder="身份证号" class="w-full p-2 border rounded"></div>
<div class="mb-2"><input type="password" v-model="addUserForm.password" placeholder="密码" class="w-full p-2 border rounded"></div>
<div class="mb-2">
<label><input type="radio" v-model="addUserForm.gender" value="male"> </label>
<label><input type="radio" v-model="addUserForm.gender" value="female"> </label>
</div>
<button type="submit" class="w-full bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">确认添加</button>
</form>
</div>
</div>
<!-- Rooms -->
<div v-if="currentView === 'rooms'">
<h2 class="text-xl font-semibold mb-4">房间信息</h2>
<table class="min-w-full bg-white">
<thead>
<tr>
<th>ID</th>
<th>类型</th>
<th>价格</th>
<th>特色</th>
<th>余量</th>
<th>类型ID</th>
</tr>
</thead>
<tbody>
<tr v-for="r in rooms" :key="r.RoomID">
<td class="border px-4 py-2 text-center">{{ r.RoomID }}</td>
<td class="border px-4 py-2 text-center">{{ r.Type }}</td>
<td class="border px-4 py-2 text-center">{{ r.Price }}</td>
<td class="border px-4 py-2 text-center">{{ r.Feature || '-' }}</td>
<td class="border px-4 py-2 text-center">{{ r.AvailableCount }}</td>
<td class="border px-4 py-2 text-center">{{ r.TypeID }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Hotel Types -->
<div v-if="currentView === 'hoteltypes'">
<h2 class="text-xl font-semibold mb-4">酒店类型</h2>
<table class="min-w-full bg-white">
<thead>
<tr>
<th>类型ID</th>
<th>类型名称</th>
<th>星级</th>
</tr>
</thead>
<tbody>
<tr v-for="t in hotelTypes" :key="t.TypeID">
<td class="border px-4 py-2 text-center">{{ t.TypeID }}</td>
<td class="border px-4 py-2 text-center">{{ t.TypeName }}</td>
<td class="border px-4 py-2 text-center">{{ t.StarRating }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Reservations -->
<div v-if="currentView === 'reservations'">
<h2 class="text-xl font-semibold mb-4">预订信息</h2>
<table class="min-w-full bg-white">
<thead>
<tr>
<th>预订ID</th>
<th>客户姓名</th>
<th>房间ID</th>
<th>入住时间</th>
<th>入住天数</th>
</tr>
</thead>
<tbody>
<tr v-for="res in reservations" :key="res.ReservationID">
<td class="border px-4 py-2 text-center">{{ res.ReservationID }}</td>
<td class="border px-4 py-2 text-center">{{ res.Name }}</td>
<td class="border px-4 py-2 text-center">{{ res.RoomID }}</td>
<td class="border px-4 py-2 text-center">{{ new Date(res.CheckInTime).toLocaleDateString() }}</td>
<td class="border px-4 py-2 text-center">{{ res.StayDays }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
const message = ref('');
const currentView = ref('customers');
const showAddUser = ref(false);
const tabs = [
{ key: 'customers', name: '客户管理' },
{ key: 'rooms', name: '房间管理' },
{ key: 'hoteltypes', name: '酒店类型' },
{ key: 'reservations', name: '预订管理' },
];
const customers = ref([]);
const rooms = ref([]);
const hotelTypes = ref([]);
const reservations = ref([]);
const addUserForm = reactive({
name: '',
gender: 'male',
contact: '',
idCard: '',
password: '',
});
async function loadAllData() {
try {
[customers.value, rooms.value, hotelTypes.value, reservations.value] = await Promise.all([
$fetch('/api/customers'),
$fetch('/api/rooms/all'),
$fetch('/api/hoteltypes'),
$fetch('/api/reservations'),
]);
} catch (error: any) {
message.value = '加载数据失败: ' + (error.data?.message || error.message);
}
}
async function addUser() {
if (!addUserForm.name || !addUserForm.contact || !addUserForm.idCard || !addUserForm.password) {
message.value = '请填写完整信息';
return;
}
try {
const response = await $fetch('/api/customers', {
method: 'POST',
body: addUserForm
});
message.value = response.message;
showAddUser.value = false;
Object.assign(addUserForm, { name: '', gender: 'male', contact: '', idCard: '', password: '' }); // Reset form
loadAllData(); // Refresh data
} catch (error: any) {
message.value = '添加失败: ' + (error.data?.message || error.message);
}
}
onMounted(() => {
loadAllData();
});
</script>

View File

@ -0,0 +1,24 @@
import { defineEventHandler, createError } from 'h3';
import { db } from '~/server/db';
import { customers } from '~/server/db/schema';
export default defineEventHandler(async () => {
try {
// Select specific fields to avoid exposing sensitive data like passwords
const allCustomers = await db.select({
CustomerID: customers.id,
Name: customers.name,
Contact: customers.contact,
Gender: customers.gender,
IDCard: customers.idCard,
}).from(customers);
return allCustomers;
} catch (error) {
console.error('Error fetching customers:', error);
return createError({
statusCode: 500,
statusMessage: 'Failed to fetch customers',
});
}
});

View File

@ -0,0 +1,39 @@
import { defineEventHandler, readBody, setResponseStatus, createError } from 'h3';
import { db, customers } from '~/server/db';
import bcrypt from 'bcryptjs';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { name, gender, contact, idCard, password } = body;
if (!name || !gender || !contact || !idCard || !password) {
return createError({ statusCode: 400, statusMessage: '请填写完整信息' });
}
try {
const hashedPassword = bcrypt.hashSync(password, 10);
await db.insert(customers).values({
name,
gender,
contact,
idCard,
password: hashedPassword,
});
return { message: '用户添加成功!' };
} catch (error: any) {
if (error.message?.includes('UNIQUE constraint failed')) {
if (error.message.includes('customers.contact')) {
return createError({ statusCode: 409, statusMessage: '该手机号已被注册' });
}
if (error.message.includes('customers.id_card')) {
return createError({ statusCode: 409, statusMessage: '该身份证号已被注册' });
}
}
console.error('Add user error:', error);
return createError({ statusCode: 500, statusMessage: '添加用户失败,请稍后重试' });
}
});

View File

@ -0,0 +1,21 @@
import { defineEventHandler, createError } from 'h3';
import { db } from '~/server/db';
import { roomTypes } from '~/server/db/schema';
export default defineEventHandler(async () => {
try {
const allRoomTypes = await db.select({
TypeID: roomTypes.id,
TypeName: roomTypes.typeName,
StarRating: roomTypes.starRating,
}).from(roomTypes);
return allRoomTypes;
} catch (error) {
console.error('Error fetching hotel types:', error);
return createError({
statusCode: 500,
statusMessage: 'Failed to fetch hotel types',
});
}
});

View File

@ -0,0 +1,27 @@
import { defineEventHandler, createError } from 'h3';
import { db } from '~/server/db';
import { reservations, customers } from '~/server/db/schema';
import { eq } from 'drizzle-orm';
export default defineEventHandler(async () => {
try {
const allReservations = await db
.select({
ReservationID: reservations.id,
Name: customers.name,
RoomID: reservations.roomId,
CheckInTime: reservations.checkInTime,
StayDays: reservations.stayDays,
})
.from(reservations)
.leftJoin(customers, eq(reservations.customerId, customers.id));
return allReservations;
} catch (error) {
console.error('Error fetching all reservations:', error);
return createError({
statusCode: 500,
statusMessage: 'Failed to fetch all reservations',
});
}
});

View File

@ -0,0 +1,28 @@
import { defineEventHandler, createError } from 'h3';
import { db } from '~/server/db';
import { rooms, roomTypes } from '~/server/db/schema';
import { eq } from 'drizzle-orm';
export default defineEventHandler(async () => {
try {
const allRooms = await db
.select({
RoomID: rooms.id,
Type: roomTypes.typeName,
Price: rooms.price,
Feature: rooms.feature,
AvailableCount: rooms.availableCount,
TypeID: rooms.typeId,
})
.from(rooms)
.leftJoin(roomTypes, eq(rooms.typeId, roomTypes.id));
return allRooms;
} catch (error) {
console.error('Error fetching all rooms:', error);
return createError({
statusCode: 500,
statusMessage: 'Failed to fetch all rooms',
});
}
});