Переглянути джерело

feat:房间管理页面更新

yangyi 4 місяців тому
батько
коміт
f28ea276a4

+ 5 - 5
src/api/admin-appointment-controller.ts

@@ -6,7 +6,7 @@
  */
 import type {
   HandleCancelDto,
-  ListAllParams,
+  ListAll1Params,
   RBoolean,
   RPageVoAppointmentVo,
   ReviewDto,
@@ -39,14 +39,14 @@ export const getAdminAppointmentController = () => {
   /**
    * @summary 获取全部预约(管理员)
    */
-  const listAll = (params?: ListAllParams) => {
+  const listAll1 = (params?: ListAll1Params) => {
     return customAxiosInstance<RPageVoAppointmentVo>({
       url: `/admin/appointments`,
       method: "GET",
       params,
     });
   };
-  return { review, handleCancel, listAll };
+  return { review, handleCancel, listAll1 };
 };
 export type ReviewResult = NonNullable<
   Awaited<
@@ -58,8 +58,8 @@ export type HandleCancelResult = NonNullable<
     ReturnType<ReturnType<typeof getAdminAppointmentController>["handleCancel"]>
   >
 >;
-export type ListAllResult = NonNullable<
+export type ListAll1Result = NonNullable<
   Awaited<
-    ReturnType<ReturnType<typeof getAdminAppointmentController>["listAll"]>
+    ReturnType<ReturnType<typeof getAdminAppointmentController>["listAll1"]>
   >
 >;

+ 136 - 2
src/api/admin-boarding-room-controller.ts

@@ -4,7 +4,17 @@
  * OpenAPI definition
  * OpenAPI spec version: v0
  */
-import type { RBoolean, RoomStatusDto } from "./models";
+import type {
+  BatchRoomStatusDto,
+  BoardingRoomCreateDto,
+  GetStatus1Params,
+  RBoardingRoomVo,
+  RBoolean,
+  RListBoardingRoomVo,
+  RListRoomStatusVo,
+  RTodayStatusVo,
+  RoomStatusDto,
+} from "./models";
 import { customAxiosInstance } from "../util/axios-instance";
 
 export const getAdminBoardingRoomController = () => {
@@ -19,7 +29,92 @@ export const getAdminBoardingRoomController = () => {
       data: roomStatusDto,
     });
   };
-  return { updateStatus1 };
+  /**
+   * @summary 批量修改日期范围房态
+   */
+  const batchUpdateStatus = (
+    roomId: number,
+    batchRoomStatusDto: BatchRoomStatusDto,
+  ) => {
+    return customAxiosInstance<RBoolean>({
+      url: `/admin/boarding-rooms/${roomId}/status/batch`,
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      data: batchRoomStatusDto,
+    });
+  };
+  /**
+   * @summary 修改房型信息
+   */
+  const update2 = (
+    id: number,
+    boardingRoomCreateDto: BoardingRoomCreateDto,
+  ) => {
+    return customAxiosInstance<RBoolean>({
+      url: `/admin/boarding-rooms/${id}`,
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      data: boardingRoomCreateDto,
+    });
+  };
+  /**
+   * @summary 删除房型(逻辑删除)
+   */
+  const delete2 = (id: number) => {
+    return customAxiosInstance<RBoolean>({
+      url: `/admin/boarding-rooms/${id}`,
+      method: "DELETE",
+    });
+  };
+  /**
+   * @summary 获取所有房型(含已禁用)
+   */
+  const listAll = () => {
+    return customAxiosInstance<RListBoardingRoomVo>({
+      url: `/admin/boarding-rooms`,
+      method: "GET",
+    });
+  };
+  /**
+   * @summary 新增房型
+   */
+  const create3 = (boardingRoomCreateDto: BoardingRoomCreateDto) => {
+    return customAxiosInstance<RBoardingRoomVo>({
+      url: `/admin/boarding-rooms`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: boardingRoomCreateDto,
+    });
+  };
+  /**
+   * @summary 获取房型某月状态(管理员)
+   */
+  const getStatus1 = (id: number, params: GetStatus1Params) => {
+    return customAxiosInstance<RListRoomStatusVo>({
+      url: `/admin/boarding-rooms/${id}/status`,
+      method: "GET",
+      params,
+    });
+  };
+  /**
+   * @summary 今日所有房型入住概览
+   */
+  const getTodayStatus = () => {
+    return customAxiosInstance<RTodayStatusVo>({
+      url: `/admin/boarding-rooms/today-status`,
+      method: "GET",
+    });
+  };
+  return {
+    updateStatus1,
+    batchUpdateStatus,
+    update2,
+    delete2,
+    listAll,
+    create3,
+    getStatus1,
+    getTodayStatus,
+  };
 };
 export type UpdateStatus1Result = NonNullable<
   Awaited<
@@ -28,3 +123,42 @@ export type UpdateStatus1Result = NonNullable<
     >
   >
 >;
+export type BatchUpdateStatusResult = NonNullable<
+  Awaited<
+    ReturnType<
+      ReturnType<typeof getAdminBoardingRoomController>["batchUpdateStatus"]
+    >
+  >
+>;
+export type Update2Result = NonNullable<
+  Awaited<
+    ReturnType<ReturnType<typeof getAdminBoardingRoomController>["update2"]>
+  >
+>;
+export type Delete2Result = NonNullable<
+  Awaited<
+    ReturnType<ReturnType<typeof getAdminBoardingRoomController>["delete2"]>
+  >
+>;
+export type ListAllResult = NonNullable<
+  Awaited<
+    ReturnType<ReturnType<typeof getAdminBoardingRoomController>["listAll"]>
+  >
+>;
+export type Create3Result = NonNullable<
+  Awaited<
+    ReturnType<ReturnType<typeof getAdminBoardingRoomController>["create3"]>
+  >
+>;
+export type GetStatus1Result = NonNullable<
+  Awaited<
+    ReturnType<ReturnType<typeof getAdminBoardingRoomController>["getStatus1"]>
+  >
+>;
+export type GetTodayStatusResult = NonNullable<
+  Awaited<
+    ReturnType<
+      ReturnType<typeof getAdminBoardingRoomController>["getTodayStatus"]
+    >
+  >
+>;

+ 4 - 4
src/api/admin-schedule-controller.ts

@@ -38,13 +38,13 @@ export const getAdminScheduleController = () => {
   /**
    * @summary 删除排班
    */
-  const delete2 = (id: number) => {
+  const delete3 = (id: number) => {
     return customAxiosInstance<RBoolean>({
       url: `/admin/schedules/${id}`,
       method: "DELETE",
     });
   };
-  return { list2, create1, delete2 };
+  return { list2, create1, delete3 };
 };
 export type List2Result = NonNullable<
   Awaited<ReturnType<ReturnType<typeof getAdminScheduleController>["list2"]>>
@@ -52,6 +52,6 @@ export type List2Result = NonNullable<
 export type Create1Result = NonNullable<
   Awaited<ReturnType<ReturnType<typeof getAdminScheduleController>["create1"]>>
 >;
-export type Delete2Result = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getAdminScheduleController>["delete2"]>>
+export type Delete3Result = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getAdminScheduleController>["delete3"]>>
 >;

+ 13 - 0
src/api/models/batchRoomStatusDto.ts

@@ -0,0 +1,13 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export interface BatchRoomStatusDto {
+  endDate: string;
+  startDate: string;
+  /** @minLength 1 */
+  status: string;
+}

+ 15 - 0
src/api/models/boardingRoomCreateDto.ts

@@ -0,0 +1,15 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export interface BoardingRoomCreateDto {
+  capacity?: number;
+  description?: string;
+  pricePerDay: number;
+  /** @minLength 1 */
+  roomName: string;
+  roomType?: string;
+}

+ 10 - 0
src/api/models/getStatus1Params.ts

@@ -0,0 +1,10 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type GetStatus1Params = {
+  month: string;
+};

+ 8 - 1
src/api/models/index.ts

@@ -8,18 +8,21 @@
 export * from "./adminUserDetailVo";
 export * from "./appointmentDto";
 export * from "./appointmentVo";
+export * from "./batchRoomStatusDto";
+export * from "./boardingRoomCreateDto";
 export * from "./boardingRoomVo";
 export * from "./cancelBody";
 export * from "./deleteByIdParams";
 export * from "./dewormingDto";
 export * from "./dewormingVo";
+export * from "./getStatus1Params";
 export * from "./getStatusParams";
 export * from "./handleCancelDto";
 export * from "./list2Params";
 export * from "./list3Params";
 export * from "./list4Params";
 export * from "./list5Params";
-export * from "./listAllParams";
+export * from "./listAll1Params";
 export * from "./loginDto";
 export * from "./packageDto";
 export * from "./packageVo";
@@ -30,6 +33,7 @@ export * from "./petVo";
 export * from "./queryByPageUsersParams";
 export * from "./rAdminUserDetailVo";
 export * from "./rAppointmentVo";
+export * from "./rBoardingRoomVo";
 export * from "./rBoolean";
 export * from "./rDewormingVo";
 export * from "./rListAppointmentVo";
@@ -45,6 +49,7 @@ export * from "./rPageVoAppointmentVo";
 export * from "./rPageVoUserVo";
 export * from "./rPetVo";
 export * from "./rScheduleVo";
+export * from "./rTodayStatusVo";
 export * from "./rUserVo";
 export * from "./rVaccinationVo";
 export * from "./rVoid";
@@ -54,6 +59,8 @@ export * from "./roomStatusDto";
 export * from "./roomStatusVo";
 export * from "./scheduleDto";
 export * from "./scheduleVo";
+export * from "./todayGuestVo";
+export * from "./todayStatusVo";
 export * from "./updateUserAvatarDto";
 export * from "./updateUserPasswordDto";
 export * from "./updateUserStatusDto";

+ 1 - 1
src/api/models/listAllParams.ts → src/api/models/listAll1Params.ts

@@ -5,7 +5,7 @@
  * OpenAPI spec version: v0
  */
 
-export type ListAllParams = {
+export type ListAll1Params = {
   pageNum?: number;
   pageSize?: number;
   status?: string;

+ 13 - 0
src/api/models/rBoardingRoomVo.ts

@@ -0,0 +1,13 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { BoardingRoomVo } from "./boardingRoomVo";
+
+export interface RBoardingRoomVo {
+  code?: number;
+  data?: BoardingRoomVo;
+  message?: string;
+}

+ 13 - 0
src/api/models/rTodayStatusVo.ts

@@ -0,0 +1,13 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { TodayStatusVo } from "./todayStatusVo";
+
+export interface RTodayStatusVo {
+  code?: number;
+  data?: TodayStatusVo;
+  message?: string;
+}

+ 12 - 0
src/api/models/todayGuestVo.ts

@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export interface TodayGuestVo {
+  ownerName?: string;
+  petName?: string;
+  roomName?: string;
+}

+ 15 - 0
src/api/models/todayStatusVo.ts

@@ -0,0 +1,15 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { TodayGuestVo } from "./todayGuestVo";
+
+export interface TodayStatusVo {
+  availableCount?: number;
+  maintenanceCount?: number;
+  occupiedCount?: number;
+  todayGuests?: TodayGuestVo[];
+  totalRooms?: number;
+}

+ 5 - 4
src/components/PaymentDialog.vue

@@ -29,7 +29,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
 import { ElMessage } from 'element-plus'
 
 const props = defineProps<{
@@ -44,7 +44,10 @@ const emit = defineEmits<{
   close: []
 }>()
 
-const visible = ref(props.visible)
+const visible = computed({
+  get: () => props.visible,
+  set: (val: boolean) => emit('update:visible', val)
+})
 const paying = ref(false)
 const paymentMethod = ref('wechat')
 
@@ -58,7 +61,6 @@ const handlePay = async () => {
       ElMessage.success('支付成功')
       emit('success')
       visible.value = false
-      emit('update:visible', false)
     } else {
       ElMessage.error(res.message || '支付失败')
     }
@@ -71,7 +73,6 @@ const handlePay = async () => {
 
 const handleClose = () => {
   visible.value = false
-  emit('update:visible', false)
   emit('close')
 }
 </script>

+ 16 - 8
src/components/RoomCalendar.vue

@@ -2,7 +2,7 @@
   <div class="room-calendar">
     <div class="calendar-header">
       <el-button text @click="prevMonth">&lt;</el-button>
-      <span class="month-label">{{ currentYear }}年{{ currentMonth }}月</span>
+      <span class="month-label">{{ year }}年{{ month }}月</span>
       <el-button text @click="nextMonth">&gt;</el-button>
     </div>
 
@@ -39,18 +39,20 @@ const props = defineProps<{
 
 const emit = defineEmits<{
   'date-click': [date: string, status: 'AVAILABLE' | 'OCCUPIED']
+  'month-change': [month: string]
 }>()
 
 const weekDays = ['日', '一', '二', '三', '四', '五', '六']
 
-const [currentYear, currentMonth] = props.month.split('-').map(Number)
-
 interface DayInfo {
   day: number
   date: string
   status?: 'AVAILABLE' | 'OCCUPIED'
 }
 
+const year = computed(() => Number(props.month.split('-')[0]))
+const month = computed(() => Number(props.month.split('-')[1]))
+
 const statusMap = computed(() => {
   const map = new Map<string, 'AVAILABLE' | 'OCCUPIED'>()
   for (const item of props.statusList) {
@@ -60,8 +62,10 @@ const statusMap = computed(() => {
 })
 
 const calendarDays = computed(() => {
-  const daysInMonth = new Date(currentYear, currentMonth, 0).getDate()
-  const firstDayOfWeek = new Date(currentYear, currentMonth - 1, 1).getDay()
+  const y = year.value
+  const m = month.value
+  const daysInMonth = new Date(y, m, 0).getDate()
+  const firstDayOfWeek = new Date(y, m - 1, 1).getDay()
   const weeks: (DayInfo | null)[][] = []
   let week: (DayInfo | null)[] = []
 
@@ -70,7 +74,7 @@ const calendarDays = computed(() => {
   }
 
   for (let day = 1; day <= daysInMonth; day++) {
-    const dateStr = `${currentYear}-${String(currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`
+    const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(day).padStart(2, '0')}`
     week.push({
       day,
       date: dateStr,
@@ -105,11 +109,15 @@ const handleDayClick = (day: DayInfo | null) => {
 }
 
 const prevMonth = () => {
-  // TODO: emit month change
+  const d = new Date(year.value, month.value - 2, 1)
+  const newMonth = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
+  emit('month-change', newMonth)
 }
 
 const nextMonth = () => {
-  // TODO: emit month change
+  const d = new Date(year.value, month.value, 1)
+  const newMonth = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
+  emit('month-change', newMonth)
 }
 </script>
 

+ 20 - 0
src/layout/AdminLayout.vue

@@ -142,4 +142,24 @@ const logoutHandler = async () => {
   justify-content: center;
   margin: 16px 8px;
 }
+
+@media (max-width: 768px) {
+  .menu {
+    width: 64px;
+  }
+
+  .menu.collapsed {
+    width: 0;
+    overflow: hidden;
+    border: none;
+  }
+
+  .admin-title {
+    font-size: 14px;
+  }
+
+  .title {
+    font-size: 16px;
+  }
+}
 </style>

+ 16 - 0
src/layout/UserLayout.vue

@@ -151,4 +151,20 @@ const logoutHandler = async () => {
   font-size: 20px;
   color: var(--el-color-primary);
 }
+
+@media (max-width: 768px) {
+  .header {
+    flex-wrap: wrap;
+    padding: 0 12px;
+  }
+
+  .title {
+    font-size: 16px;
+  }
+
+  .nav-menu :deep(.el-menu-item) {
+    font-size: 12px;
+    padding: 0 10px;
+  }
+}
 </style>

+ 2 - 2
src/style.css

@@ -46,12 +46,12 @@ a:hover {
 }
 
 .el-button--primary {
-  background: linear-gradient(135deg, #db2777, #be185d);
+  background: linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-dark-2));
   border: none;
 }
 
 .el-button--primary:hover {
-  background: linear-gradient(135deg, #e34889, #c2185b);
+  background: linear-gradient(135deg, var(--el-color-primary-light-3), var(--el-color-primary-dark-2));
   opacity: 0.9;
 }
 

+ 1 - 0
src/util/axios-instance.ts

@@ -10,6 +10,7 @@ export interface CustomInstanceConfig extends AxiosRequestConfig {
 const axiosInstance: AxiosInstance = axios.create({
     baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
     timeout: 30000,
+    withCredentials: true,
     headers: {
         'Content-Type': 'application/json',
     },

+ 164 - 3
src/view/HomeView.vue

@@ -1,12 +1,173 @@
 <template>
-<el-text>
-  这是基于Vite + Vue + TypeScript构建的前端模板
-</el-text>
+  <div class="home">
+    <section class="hero">
+      <div class="hero-content">
+        <h1>宠物服务管理系统</h1>
+        <p class="hero-subtitle">专业宠物寄养与洗护服务平台</p>
+      </div>
+    </section>
+
+    <section class="services">
+      <el-card class="service-card" shadow="hover" @click="goBoarding">
+        <div class="service-icon">🏠</div>
+        <h2>寄养服务</h2>
+        <p>专业宠物寄养,安心托管</p>
+        <el-button type="primary" round>查看详情</el-button>
+      </el-card>
+      <el-card class="service-card" shadow="hover" @click="goGrooming">
+        <div class="service-icon">✂️</div>
+        <h2>洗护服务</h2>
+        <p>专业宠物洗护,焕然一新</p>
+        <el-button type="primary" round>查看详情</el-button>
+      </el-card>
+    </section>
+
+    <section class="features">
+      <h2>平台优势</h2>
+      <div class="feature-grid">
+        <div class="feature-item">
+          <h3>专业团队</h3>
+          <p>经验丰富的宠物护理团队</p>
+        </div>
+        <div class="feature-item">
+          <h3>安全环境</h3>
+          <p>24 小时监控,安心托管</p>
+        </div>
+        <div class="feature-item">
+          <h3>智能管理</h3>
+          <p>在线预约,实时查看房态</p>
+        </div>
+        <div class="feature-item">
+          <h3>贴心服务</h3>
+          <p>个性化护理方案</p>
+        </div>
+      </div>
+    </section>
+  </div>
 </template>
 
 <script setup lang="ts">
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+const goBoarding = () => router.push('/booking/boarding')
+const goGrooming = () => router.push('/booking/grooming')
 </script>
 
 <style scoped>
+.home {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 40px 20px;
+}
+
+.hero {
+  text-align: center;
+  padding: 60px 20px;
+}
+
+.hero h1 {
+  font-size: 36px;
+  color: var(--el-color-primary-dark-2);
+  margin-bottom: 16px;
+}
+
+.hero-subtitle {
+  font-size: 18px;
+  color: #666;
+}
+
+.services {
+  display: flex;
+  gap: 40px;
+  justify-content: center;
+  margin: 40px 0;
+}
+
+.service-card {
+  width: 320px;
+  text-align: center;
+  padding: 40px 20px;
+  cursor: pointer;
+  transition: transform 0.2s;
+}
+
+.service-card:hover {
+  transform: translateY(-4px);
+}
+
+.service-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+.service-card h2 {
+  color: var(--el-color-primary-dark-2);
+  margin-bottom: 12px;
+}
+
+.service-card p {
+  color: #666;
+  margin-bottom: 20px;
+}
+
+.features {
+  text-align: center;
+  margin: 60px 0;
+}
+
+.features h2 {
+  font-size: 28px;
+  color: var(--el-color-primary-dark-2);
+  margin-bottom: 32px;
+}
+
+.feature-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 24px;
+}
+
+.feature-item {
+  padding: 24px;
+  border-radius: 8px;
+  background: var(--el-color-primary-light-7);
+}
+
+.feature-item h3 {
+  color: var(--el-color-primary-dark-2);
+  margin-bottom: 8px;
+}
+
+.feature-item p {
+  color: #666;
+  font-size: 14px;
+}
+
+@media (max-width: 768px) {
+  .hero h1 {
+    font-size: 24px;
+  }
+
+  .services {
+    flex-direction: column;
+    align-items: center;
+  }
+
+  .service-card {
+    width: 100%;
+    max-width: 400px;
+  }
+
+  .feature-grid {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
 
+@media (max-width: 480px) {
+  .feature-grid {
+    grid-template-columns: 1fr;
+  }
+}
 </style>

+ 19 - 19
src/view/LoginView.vue

@@ -259,7 +259,7 @@ const handleRegister = async () => {
   justify-content: center;
   align-items: center;
   min-height: 100vh;
-  background: linear-gradient(135deg, #fdf2f8 0%, #fce7f3 50%, #fbcfe8 100%);
+  background: linear-gradient(135deg, var(--el-color-primary-light-7) 0%, var(--el-color-primary-light-5) 50%, var(--el-color-primary-light-3) 100%);
   position: relative;
   overflow: hidden;
 }
@@ -279,7 +279,7 @@ const handleRegister = async () => {
 .circle-1 {
   width: 400px;
   height: 400px;
-  background: #db2777;
+  background: var(--el-color-primary-dark-2);
   top: -100px;
   right: -100px;
   animation: float 20s ease-in-out infinite;
@@ -288,7 +288,7 @@ const handleRegister = async () => {
 .circle-2 {
   width: 300px;
   height: 300px;
-  background: #f472b6;
+  background: var(--el-color-primary-light-3);
   bottom: -80px;
   left: -80px;
   animation: float 15s ease-in-out infinite reverse;
@@ -329,7 +329,7 @@ const handleRegister = async () => {
 
 .auth-card {
   border-radius: 16px;
-  border: 1px solid rgba(219, 39, 119, 0.1);
+  border: 1px solid color-mix(in srgb, var(--el-color-primary) 15%, transparent);
   background: rgba(255, 255, 255, 0.9);
   backdrop-filter: blur(10px);
   padding: 8px;
@@ -339,14 +339,14 @@ const handleRegister = async () => {
   position: absolute;
   top: 16px;
   left: 16px;
-  color: #be185d;
+  color: var(--el-color-primary-dark-2);
   font-size: 14px;
   cursor: pointer;
   transition: color 0.2s;
 }
 
 .back-btn:hover {
-  color: #db2777;
+  color: var(--el-color-primary);
 }
 
 .back-btn .el-icon {
@@ -362,14 +362,14 @@ const handleRegister = async () => {
   margin: 0 0 8px;
   font-size: 28px;
   font-weight: 600;
-  color: #831843;
+  color: var(--el-color-primary-dark-2);
   letter-spacing: 2px;
 }
 
 .auth-subtitle {
   margin: 0;
   font-size: 14px;
-  color: #be185d;
+  color: var(--el-color-primary-dark-2);
 }
 
 .auth-tabs {
@@ -383,22 +383,22 @@ const handleRegister = async () => {
 :deep(.auth-tabs .el-tabs__item) {
   font-size: 16px;
   font-weight: 500;
-  color: #831843;
+  color: var(--el-color-primary-dark-2);
   transition: color 0.2s;
 }
 
 :deep(.auth-tabs .el-tabs__item.is-active) {
-  color: #db2777;
+  color: var(--el-color-primary);
 }
 
 :deep(.auth-tabs .el-tabs__active-bar) {
-  background-color: #db2777;
+  background-color: var(--el-color-primary);
   height: 3px;
   border-radius: 3px;
 }
 
 :deep(.auth-tabs .el-tabs__nav-wrap::after) {
-  background-color: rgba(219, 39, 119, 0.15);
+  background-color: color-mix(in srgb, var(--el-color-primary) 15%, transparent);
   height: 1px;
 }
 
@@ -409,20 +409,20 @@ const handleRegister = async () => {
 :deep(.el-input__wrapper) {
   border-radius: 10px;
   padding: 8px 12px;
-  box-shadow: 0 0 0 1px rgba(219, 39, 119, 0.15) inset;
+  box-shadow: 0 0 0 1px color-mix(in srgb, var(--el-color-primary) 15%, transparent) inset;
   transition: box-shadow 0.2s;
 }
 
 :deep(.el-input__wrapper:hover) {
-  box-shadow: 0 0 0 1px rgba(219, 39, 119, 0.3) inset;
+  box-shadow: 0 0 0 1px color-mix(in srgb, var(--el-color-primary) 30%, transparent) inset;
 }
 
 :deep(.el-input__wrapper.is-focus) {
-  box-shadow: 0 0 0 1px #db2777 inset;
+  box-shadow: 0 0 0 1px var(--el-color-primary) inset;
 }
 
 :deep(.el-input__prefix) {
-  color: #db2777;
+  color: var(--el-color-primary);
   margin-right: 8px;
 }
 
@@ -432,15 +432,15 @@ const handleRegister = async () => {
   font-size: 16px;
   font-weight: 500;
   letter-spacing: 4px;
-  background: linear-gradient(135deg, #db2777 0%, #be185d 100%);
+  background: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-dark-2) 100%);
   border: none;
   transition: all 0.2s;
 }
 
 .auth-submit:hover {
-  background: linear-gradient(135deg, #be185d 0%, #9d174d 100%);
+  background: linear-gradient(135deg, var(--el-color-primary-dark-2) 0%, var(--el-color-primary-dark) 100%);
   transform: translateY(-1px);
-  box-shadow: 0 4px 12px rgba(219, 39, 119, 0.3);
+  box-shadow: 0 4px 12px color-mix(in srgb, var(--el-color-primary) 30%, transparent);
 }
 
 .auth-submit:active {

+ 8 - 6
src/view/admin/AdminOrderListView.vue

@@ -89,7 +89,7 @@ import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import type { Appointment } from '@/type'
 import { getAdminAppointmentController } from '@/api/admin-appointment-controller'
-import type { ListAllParams, ReviewDto, HandleCancelDto } from '@/api/models'
+import type { ListAll1Params, ReviewDto, HandleCancelDto } from '@/api/models'
 
 const router = useRouter()
 const orders = ref<Appointment[]>([])
@@ -116,14 +116,16 @@ const fetchOrders = async () => {
   loading.value = true
   try {
     const api = getAdminAppointmentController()
-    const params: ListAllParams = {
+    const params: ListAll1Params = {
       pageNum: pageNum.value,
       pageSize: pageSize.value
     }
     if (statusFilter.value) params.status = statusFilter.value
-    const res = await api.listAll(params)
-    if (res.code === 200 && res.data) {
-      orders.value = (res.data.data ?? []).map(a => ({
+    const res = await api.listAll1(params)
+    const page = res.data
+    if (res.code === 200 && page) {
+      const list = page.data ?? []
+      orders.value = list.map((a) => ({
         id: String(a.id ?? ''),
         userId: String(a.userId ?? ''),
         petId: String(a.petId ?? ''),
@@ -140,7 +142,7 @@ const fetchOrders = async () => {
         rejectReason: a.rejectReason,
         createdAt: a.createdAt ?? ''
       }))
-      total.value = res.data.total ?? 0
+      total.value = page.total ?? 0
     }
   } catch (err: any) {
     ElMessage.error(err.message || '加载失败')

+ 7 - 1
src/view/admin/AdminRoomCalendarView.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="admin-room-calendar">
-    <h1>房态管理</h1>
+    <h1>宠物房间管理</h1>
 
     <div class="toolbar">
       <el-select v-model="selectedRoomId" placeholder="选择房型" @change="fetchStatus">
@@ -21,6 +21,7 @@
       :month="currentMonth"
       :status-list="roomStatusList"
       @date-click="handleDateClick"
+      @month-change="onMonthChange"
     />
   </div>
 </template>
@@ -90,6 +91,11 @@ const handleDateClick = async (date: string, status: 'AVAILABLE' | 'OCCUPIED') =
   }
 }
 
+const onMonthChange = (month: string) => {
+  currentMonth.value = month
+  fetchStatus()
+}
+
 onMounted(fetchRooms)
 </script>
 

+ 1 - 1
src/view/admin/AdminScheduleListView.vue

@@ -138,7 +138,7 @@ const handleCreate = async () => {
 const handleDelete = async (id: string) => {
   try {
     const api = getAdminScheduleController()
-    const res = await api.delete2(Number(id))
+    const res = await api.delete3(Number(id))
     if (res.code === 200) {
       ElMessage.success('删除成功')
       await fetchSchedules()

+ 24 - 7
src/view/booking/BoardingBookingView.vue

@@ -60,6 +60,7 @@
           :room-id="selectedRoomId"
           :month="currentMonth"
           :status-list="roomStatusList"
+          @month-change="onCalendarMonthChange"
         />
         <p class="total-price" v-if="totalPrice > 0">
           总价:<strong>¥{{ totalPrice }}</strong>
@@ -115,7 +116,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, onMounted } from 'vue'
+import { computed, ref, watch, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import type { Pet, Package, BoardingRoom, RoomStatus } from '@/type'
@@ -143,9 +144,20 @@ const selectedRoom = computed(() => rooms.value.find(r => r.id === selectedRoomI
 const dateRange = ref<[Date, Date] | null>(null)
 const roomStatusList = ref<RoomStatus[]>([])
 const totalPrice = ref(0)
-const currentMonth = computed(() => {
-  const d = dateRange.value?.[0] || new Date()
-  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
+const currentMonth = ref(new Date().toISOString().slice(0, 7))
+
+const onCalendarMonthChange = (month: string) => {
+  currentMonth.value = month
+  if (selectedRoomId.value) {
+    fetchStatus()
+  }
+}
+
+watch(dateRange, (range) => {
+  if (range) {
+    const [start] = range
+    currentMonth.value = `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, '0')}`
+  }
 })
 
 // Step 3
@@ -215,9 +227,7 @@ const fetchPets = async () => {
   }
 }
 
-const onRoomChange = async () => {
-  dateRange.value = null
-  totalPrice.value = 0
+const fetchStatus = async () => {
   if (!selectedRoomId.value) return
   try {
     const api = getBoardingRoomController()
@@ -233,6 +243,13 @@ const onRoomChange = async () => {
   }
 }
 
+const onRoomChange = async () => {
+  dateRange.value = null
+  totalPrice.value = 0
+  if (!selectedRoomId.value) return
+  await fetchStatus()
+}
+
 const calcTotal = () => {
   if (dateRange.value && selectedRoom.value) {
     const days = Math.ceil((dateRange.value[1].getTime() - dateRange.value[0].getTime()) / 86400000)

+ 8 - 3
src/view/order/OrderDetailView.vue

@@ -97,9 +97,14 @@ const handlePayDeposit = async () => {
 
 const handleCancel = async () => {
   try {
-    await ElMessageBox.confirm('确定要申请取消该订单吗?', '提示', { type: 'warning' })
+    const { value: reason } = await ElMessageBox.prompt('请填写取消原因', '申请取消', {
+      confirmButtonText: '确认',
+      cancelButtonText: '再想想',
+      inputPlaceholder: '请输入取消原因',
+      inputValidator: (val: string) => !!val.trim() || '取消原因不能为空'
+    })
     const api = getAppointmentController()
-    const res = await api.cancel(Number(route.params.id), { reason: '用户申请取消' })
+    const res = await api.cancel(Number(route.params.id), { reason })
     if (res.code === 200) {
       ElMessage.success('取消申请已提交')
       await loadOrder()
@@ -107,7 +112,7 @@ const handleCancel = async () => {
       ElMessage.error(res.message || '取消失败')
     }
   } catch {
-    // cancelled
+    // cancelled / closed
   }
 }
 

+ 13 - 1
src/view/order/OrderListView.vue

@@ -6,6 +6,7 @@
       <el-tab-pane label="待确认" name="PENDING" />
       <el-tab-pane label="已确认" name="CONFIRMED" />
       <el-tab-pane label="已完成" name="COMPLETED" />
+      <el-tab-pane label="取消申请" name="CANCEL_REQUESTED" />
       <el-tab-pane label="已取消" name="CANCELLED" />
     </el-tabs>
 
@@ -17,7 +18,7 @@
       <el-card v-for="order in orders" :key="order.id" class="order-card">
         <div class="order-info">
           <p>订单 ID: {{ order.id }}</p>
-          <el-tag :type="statusTag(order.status)">{{ order.status }}</el-tag>
+          <el-tag :type="statusTag(order.status)">{{ statusLabel(order.status) }}</el-tag>
         </div>
         <div class="order-actions">
           <el-button size="small" @click="viewDetail(order.id)">查看详情</el-button>
@@ -78,6 +79,17 @@ const viewDetail = (id: string) => {
   router.push(`/orders/${id}`)
 }
 
+const statusLabel = (status: string) => {
+  const map: Record<string, string> = {
+    PENDING: '待审核',
+    CONFIRMED: '已确认',
+    CANCELLED: '已取消',
+    COMPLETED: '已完成',
+    CANCEL_REQUESTED: '取消申请'
+  }
+  return map[status] || status
+}
+
 const statusTag = (status: string) => {
   const map: Record<string, string> = {
     PENDING: 'warning',

+ 18 - 7
src/view/pet/PetCreateView.vue

@@ -2,20 +2,20 @@
   <div class="pet-create">
     <el-button text @click="router.back()">← 返回</el-button>
     <h1>新增宠物</h1>
-    <el-form :model="form" label-width="100px" class="form">
-      <el-form-item label="宠物名">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
+      <el-form-item label="宠物名" prop="name">
         <el-input v-model="form.name" />
       </el-form-item>
-      <el-form-item label="品种">
+      <el-form-item label="品种" prop="breed">
         <el-input v-model="form.breed" />
       </el-form-item>
-      <el-form-item label="年龄">
+      <el-form-item label="年龄" prop="age">
         <el-input-number v-model="form.age" :min="0" />
       </el-form-item>
-      <el-form-item label="体重(kg)">
+      <el-form-item label="体重(kg)" prop="weight">
         <el-input-number v-model="form.weight" :min="0" :precision="1" :step="0.5" />
       </el-form-item>
-      <el-form-item label="性别">
+      <el-form-item label="性别" prop="gender">
         <el-radio-group v-model="form.gender">
           <el-radio value="MALE">公</el-radio>
           <el-radio value="FEMALE">母</el-radio>
@@ -33,14 +33,17 @@
 </template>
 
 <script setup lang="ts">
-import { reactive } from 'vue'
+import { reactive, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
 import { getPetController } from '@/api/pet-controller'
 import type { PetDto } from '@/api/models'
 
 const router = useRouter()
 
+const formRef = ref<FormInstance>()
+
 const form = reactive({
   name: '',
   breed: '',
@@ -50,7 +53,15 @@ const form = reactive({
   notes: ''
 })
 
+const rules: FormRules = {
+  name: [{ required: true, message: '请输入宠物名', trigger: 'blur' }],
+  breed: [{ required: true, message: '请输入品种', trigger: 'blur' }],
+  weight: [{ type: 'number', min: 0, message: '体重必须大于 0', trigger: 'blur' }]
+}
+
 const handleSubmit = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
   try {
     const api = getPetController()
     const dto: PetDto = {

+ 18 - 7
src/view/pet/PetEditView.vue

@@ -2,20 +2,20 @@
   <div class="pet-edit">
     <el-button text @click="router.back()">← 返回</el-button>
     <h1>编辑宠物</h1>
-    <el-form :model="form" label-width="100px" class="form">
-      <el-form-item label="宠物名">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
+      <el-form-item label="宠物名" prop="name">
         <el-input v-model="form.name" />
       </el-form-item>
-      <el-form-item label="品种">
+      <el-form-item label="品种" prop="breed">
         <el-input v-model="form.breed" />
       </el-form-item>
-      <el-form-item label="年龄">
+      <el-form-item label="年龄" prop="age">
         <el-input-number v-model="form.age" :min="0" />
       </el-form-item>
-      <el-form-item label="体重(kg)">
+      <el-form-item label="体重(kg)" prop="weight">
         <el-input-number v-model="form.weight" :min="0" :precision="1" :step="0.5" />
       </el-form-item>
-      <el-form-item label="性别">
+      <el-form-item label="性别" prop="gender">
         <el-radio-group v-model="form.gender">
           <el-radio value="MALE">公</el-radio>
           <el-radio value="FEMALE">母</el-radio>
@@ -33,15 +33,18 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, onMounted } from 'vue'
+import { reactive, ref, onMounted } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
 import { getPetController } from '@/api/pet-controller'
 import type { PetDto } from '@/api/models'
 
 const router = useRouter()
 const route = useRoute()
 
+const formRef = ref<FormInstance>()
+
 const form = reactive({
   name: '',
   breed: '',
@@ -51,6 +54,12 @@ const form = reactive({
   notes: ''
 })
 
+const rules: FormRules = {
+  name: [{ required: true, message: '请输入宠物名', trigger: 'blur' }],
+  breed: [{ required: true, message: '请输入品种', trigger: 'blur' }],
+  weight: [{ type: 'number', min: 0, message: '体重必须大于 0', trigger: 'blur' }]
+}
+
 const fetchPet = async () => {
   try {
     const api = getPetController()
@@ -69,6 +78,8 @@ const fetchPet = async () => {
 }
 
 const handleSubmit = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
   try {
     const api = getPetController()
     const dto: PetDto = {

+ 30 - 9
src/view/profile/PasswordEditView.vue

@@ -2,14 +2,14 @@
   <div class="password-edit">
     <el-button text @click="router.back()">← 个人中心</el-button>
     <h1>修改密码</h1>
-    <el-form :model="form" label-width="100px" class="form">
-      <el-form-item label="原密码">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
+      <el-form-item label="原密码" prop="oldPassword">
         <el-input v-model="form.oldPassword" type="password" show-password />
       </el-form-item>
-      <el-form-item label="新密码">
+      <el-form-item label="新密码" prop="newPassword">
         <el-input v-model="form.newPassword" type="password" show-password />
       </el-form-item>
-      <el-form-item label="确认密码">
+      <el-form-item label="确认密码" prop="confirmPassword">
         <el-input v-model="form.confirmPassword" type="password" show-password />
       </el-form-item>
       <el-form-item>
@@ -21,9 +21,10 @@
 </template>
 
 <script setup lang="ts">
-import { reactive } from 'vue'
+import { reactive, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
 import { useLoginUserStore } from '@/store'
 import { getUserController } from '@/api/user-controller'
 import type { UpdateUserPasswordDto } from '@/api/models'
@@ -31,17 +32,37 @@ import type { UpdateUserPasswordDto } from '@/api/models'
 const router = useRouter()
 const loginUserStore = useLoginUserStore()
 
+const formRef = ref<FormInstance>()
+
 const form = reactive({
   oldPassword: '',
   newPassword: '',
   confirmPassword: ''
 })
 
-const handleSubmit = async () => {
-  if (form.newPassword !== form.confirmPassword) {
-    ElMessage.error('两次输入密码不一致')
-    return
+const validateConfirm = (_rule: any, value: string, callback: any) => {
+  if (value !== form.newPassword) {
+    callback(new Error('两次输入密码不一致'))
+  } else {
+    callback()
   }
+}
+
+const rules: FormRules = {
+  oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
+  newPassword: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 4, message: '密码长度至少 4 位', trigger: 'blur' }
+  ],
+  confirmPassword: [
+    { required: true, message: '请再次输入新密码', trigger: 'blur' },
+    { validator: validateConfirm, trigger: 'blur' }
+  ]
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
   try {
     const api = getUserController()
     const dto: UpdateUserPasswordDto = {

+ 15 - 3
src/view/profile/ProfileEditView.vue

@@ -2,8 +2,8 @@
   <div class="profile-edit">
     <el-button text @click="router.back()">← 返回</el-button>
     <h1>修改个人资料</h1>
-    <el-form :model="form" label-width="100px" class="form">
-      <el-form-item label="昵称">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
+      <el-form-item label="昵称" prop="name">
         <el-input v-model="form.name" />
       </el-form-item>
       <el-form-item>
@@ -15,9 +15,10 @@
 </template>
 
 <script setup lang="ts">
-import { reactive } from 'vue'
+import { reactive, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
 import { useLoginUserStore } from '@/store'
 import { getUserController } from '@/api/user-controller'
 import type { UserDto } from '@/api/models'
@@ -25,11 +26,22 @@ import type { UserDto } from '@/api/models'
 const router = useRouter()
 const loginUserStore = useLoginUserStore()
 
+const formRef = ref<FormInstance>()
+
 const form = reactive({
   name: loginUserStore.loginUser.user?.name ?? ''
 })
 
+const rules: FormRules = {
+  name: [
+    { required: true, message: '请输入昵称', trigger: 'blur' },
+    { min: 2, message: '昵称至少 2 个字符', trigger: 'blur' }
+  ]
+}
+
 const handleSubmit = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
   try {
     const api = getUserController()
     const dto: UserDto = {