Java全栈项目-大学生租房管理系统(4)

代码4:

import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '../store'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    redirect: '/home'
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/Login.vue')
  },
  {
    path: '/register',
    name: 'Register',
    component: () => import('../views/Register.vue')
  },
  {
    path: '/home',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/house/detail/:id',
    name: 'HouseDetail',
    component: () => import('../views/house/Detail.vue')
  },
  {
    path: '/user',
    component: () => import('../views/user/Layout.vue'),
    redirect: '/user/profile',
    meta: { requireAuth: true },
    children: [
      {
        path: 'profile',
        name: 'UserProfile',
        component: () => import('../views/user/Profile.vue')
      },
      {
        path: 'favorite',
        name: 'UserFavorite',
        component: () => import('../views/user/Favorite.vue')
      },
      {
        path: 'verification',
        name: 'UserVerification',
        component: () => import('../views/user/Verification.vue')
      }
    ]
  },
  {
    path: '/landlord',
    component: () => import('../views/landlord/Layout.vue'),
    redirect: '/landlord/house',
    meta: { requireAuth: true, roles: ['landlord'] },
    children: [
      {
        path: 'house',
        name: 'LandlordHouse',
        component: () => import('../views/landlord/House.vue')
      },
      {
        path: 'house/add',
        name: 'LandlordHouseAdd',
        component: () => import('../views/landlord/HouseForm.vue')
      },
      {
        path: 'house/edit/:id',
        name: 'LandlordHouseEdit',
        component: () => import('../views/landlord/HouseForm.vue')
      }
    ]
  },
  {
    path: '/admin',
    component: () => import('../views/admin/Layout.vue'),
    redirect: '/admin/user',
    meta: { requireAuth: true, roles: ['admin'] },
    children: [
      {
        path: 'user',
        name: 'AdminUser',
        component: () => import('../views/admin/User.vue')
      },
      {
        path: 'house',
        name: 'AdminHouse',
        component: () => import('../views/admin/House.vue')
      },
      {
        path: 'verification',
        name: 'AdminVerification',
        component: () => import('../views/admin/Verification.vue')
      }
    ]
  },
  {
    path: '*',
    name: 'NotFound',
    component: () => import('../views/NotFound.vue')
  }
]

const router = new VueRouter({
  routes
})

// 路由守卫
router.beforeEach((to, from, next) => {
  // 需要登录的页面
  if (to.matched.some(record => record.meta.requireAuth)) {
    // 未登录,跳转到登录页
    if (!store.state.isLogin) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      // 角色权限验证
      if (to.matched.some(record => record.meta.roles)) {
        const userType = store.getters.userType
        if (to.meta.roles.includes(userType)) {
          next()
        } else {
          next('/home')
        }
      } else {
        next()
      }
    }
  } else {
    next()
  }
})

export default router

frontend\src\router\modules\appointment.js

import Layout from '@/layout';

const appointmentRouter = {
  path: '/appointment',
  component: Layout,
  redirect: '/appointment/list',
  name: 'Appointment',
  meta: {
    title: '预约管理',
    icon: 'el-icon-date',
    roles: ['admin', 'user', 'landlord']
  },
  children: [
    {
      path: 'list',
      component: () => import('@/views/appointment/AppointmentList'),
      name: 'AppointmentList',
      meta: { title: '我的预约', icon: 'el-icon-tickets' }
    },
    {
      path: 'calendar',
      component: () => import('@/views/appointment/AppointmentCalendar'),
      name: 'AppointmentCalendar',
      meta: { title: '预约日程', icon: 'el-icon-date' }
    },
    {
      path: 'detail/:id',
      component: () => import('@/views/appointment/AppointmentDetail'),
      name: 'AppointmentDetail',
      meta: { title: '预约详情', activeMenu: '/appointment/list' },
      hidden: true
    }
  ]
};

export default appointmentRouter;
3.4 Store Configuration

frontend\src\store\index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token: localStorage.getItem('token') || '',
    userInfo: JSON.parse(localStorage.getItem('userInfo')) || {},
    isLogin: localStorage.getItem('token') ? true : false
  },
  mutations: {
    // 设置用户信息
    SET_USER_INFO(state, data) {
      state.token = data.token
      state.userInfo = data.userInfo
      state.isLogin = true
      // 保存到本地存储
      localStorage.setItem('token', data.token)
      localStorage.setItem('userInfo', JSON.stringify(data.userInfo))
    },
    // 清除用户信息
    CLEAR_USER_INFO(state) {
      state.token = ''
      state.userInfo = {}
      state.isLogin = false
      // 清除本地存储
      localStorage.removeItem('token')
      localStorage.removeItem('userInfo')
    },
    // 更新用户信息
    UPDATE_USER_INFO(state, userInfo) {
      state.userInfo = { ...state.userInfo, ...userInfo }
      localStorage.setItem('userInfo', JSON.stringify(state.userInfo))
    }
  },
  actions: {
    // 登录
    login({ commit }, userInfo) {
      commit('SET_USER_INFO', userInfo)
    },
    // 退出登录
    logout({ commit }) {
      commit('CLEAR_USER_INFO')
    },
    // 更新用户信息
    updateUserInfo({ commit }, userInfo) {
      commit('UPDATE_USER_INFO', userInfo)
    }
  },
  getters: {
    // 获取用户ID
    userId: state => {
      return state.userInfo.id
    },
    // 获取用户名
    username: state => {
      return state.userInfo.username
    },
    // 获取用户类型
    userType: state => {
      return state.userInfo.userType
    },
    // 是否是管理员
    isAdmin: state => {
      return state.userInfo.userType === 'admin'
    },
    // 是否是房东
    isLandlord: state => {
      return state.userInfo.userType === 'landlord'
    },
    // 是否是学生
    isStudent: state => {
      return state.userInfo.userType === 'student'
    }
  }
})

frontend\src\store\modules\appointment.js

import appointmentApi from '@/api/appointment';

const state = {
  appointments: [],
  appointmentStatistics: null
};

const mutations = {
  SET_APPOINTMENTS: (state, appointments) => {
    state.appointments = appointments;
  },
  SET_APPOINTMENT_STATISTICS: (state, statistics) => {
    state.appointmentStatistics = statistics;
  }
};

const actions = {
  // 获取用户预约列表
  getUserAppointments({ commit }) {
    return new Promise((resolve, reject) => {
      appointmentApi.getUserAppointments()
        .then(response => {
          commit('SET_APPOINTMENTS', response.data);
          resolve(response);
        })
        .catch(error => {
          reject(error);
        });
    });
  },
  
  // 获取房东预约列表
  getLandlordAppointments({ commit }) {
    return new Promise((resolve, reject) => {
      appointmentApi.getLandlordAppointments()
        .then(response => {
          commit('SET_APPOINTMENTS', response.data);
          resolve(response);
        })
        .catch(error => {
          reject(error);
        });
    });
  },
  
  // 获取预约统计数据
  getAppointmentStatistics({ commit }) {
    return new Promise((resolve, reject) => {
      appointmentApi.getAppointmentStatistics()
        .then(response => {
          commit('SET_APPOINTMENT_STATISTICS', response.data);
          resolve(response);
        })
        .catch(error => {
          reject(error);
        });
    });
  }
};

export default {
  namespaced: true,
  state,
  mutations,
  actions
};

frontend\src\utils\auth.js

import Cookies from 'js-cookie';

const TokenKey = 'rental_token';
const UserKey = 'rental_user';

// Token相关操作
export function getToken() {
  return Cookies.get(TokenKey);
}

export function setToken(token) {
  return Cookies.set(TokenKey, token);
}

export function removeToken() {
  return Cookies.remove(TokenKey);
}

// 用户信息相关操作
export function getUserInfo() {
  const userStr = localStorage.getItem(UserKey);
  return userStr ? JSON.parse(userStr) : null;
}

export function setUserInfo(userInfo) {
  return localStorage.setItem(UserKey, JSON.stringify(userInfo));
}

export function removeUserInfo() {
  return localStorage.removeItem(UserKey);
}

// 判断用户角色
export function hasRole(role) {
  const userInfo = getUserInfo();
  if (!userInfo || !userInfo.roles) {
    return false;
  }
  return userInfo.roles.includes(role);
}

// 判断是否是学生
export function isStudent() {
  return hasRole('ROLE_STUDENT');
}

// 判断是否是房东
export function isLandlord() {
  return hasRole('ROLE_LANDLORD');
}

// 判断是否是管理员
export function isAdmin() {
  return hasRole('ROLE_ADMIN');
}

frontend\src\utils\date.js

/**
 * 日期格式化
 * @param {Date} date 日期
 * @param {String} fmt 格式化字符串,默认:yyyy-MM-dd
 * @returns {String} 格式化后的日期字符串
 */
export function formatDate(date, fmt = 'yyyy-MM-dd') {
  if (!date) {
    return '';
  }
  
  if (typeof date === 'string') {
    date = new Date(date);
  }
  
  if (!(date instanceof Date)) {
    return '';
  }
  
  const o = {
    'M+': date.getMonth() + 1, // 月份
    'd+': date.getDate(), // 日
    'h+': date.getHours() % 12 === 0 ? 12 : date.getHours() % 12, // 小时
    'H+': date.getHours(), // 小时
    'm+': date.getMinutes(), // 分
    's+': date.getSeconds(), // 秒
    'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
    'S': date.getMilliseconds() // 毫秒
  };
  
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  }
  
  for (let k in o) {
    if (new RegExp('(' + k + ')').test(fmt)) {
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)));
    }
  }
  
  return fmt;
}

/**
 * 计算两个日期之间的天数
 * @param {Date} date1 日期1
 * @param {Date} date2 日期2
 * @returns {Number} 天数
 */
export function daysBetween(date1, date2) {
  const time1 = date1.getTime();
  const time2 = date2.getTime();
  const diff = Math.abs(time2 - time1);
  return Math.floor(diff / (1000 * 60 * 60 * 24));
}

/**
 * 日期加减
 * @param {Date} date 日期
 * @param {Number} days 天数
 * @returns {Date} 新日期
 */
export function addDays(date, days) {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

/**
 * 获取当前月的第一天
 * @param {Date} date 日期
 * @returns {Date} 当月第一天
 */
export function getFirstDayOfMonth(date) {
  const result = new Date(date);
  result.setDate(1);
  return result;
}

/**
 * 获取当前月的最后一天
 * @param {Date} date 日期
 * @returns {Date} 当月最后一天
 */
export function getLastDayOfMonth(date) {
  const result = new Date(date);
  result.setMonth(result.getMonth() + 1);
  result.setDate(0);
  return result;
}

frontend\src\utils\request.js

import axios from 'axios';
import { Message } from 'element-ui';
import store from '@/store';
import router from '@/router';

// 创建axios实例
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API || '/api', // url = base url + request url
  timeout: 5000 // 请求超时时间
});

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 在发送请求之前做些什么
    if (store.getters.token) {
      // 让每个请求携带token
      config.headers['Authorization'] = 'Bearer ' + store.getters.token;
    }
    return config;
  },
  error => {
    // 对请求错误做些什么
    console.log(error); // for debug
    return Promise.reject(error);
  }
);

// 响应拦截器
service.interceptors.response.use(
  /**
   * 如果您想获取http信息,如头信息或状态
   * 请返回response => response
  */
  /**
   * 通过自定义代码确定请求状态
   * 这里只是一个例子
   * 您还可以通过HTTP状态码判断状态
   */
  response => {
    const res = response.data;

    // 如果自定义代码不是200,则判断为错误
    if (res.code !== 200) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      });

      // 401: 未登录或token过期
      if (res.code === 401) {
        // 重新登录
        store.dispatch('resetToken').then(() => {
          // 为了重新实例化vue-router对象,避免bug
          location.reload();
        });
      }
      return Promise.reject(new Error(res.message || 'Error'));
    } else {
      return res;
    }
  },
  error => {
    console.log('err' + error); // for debug
    let message = error.message;
    if (error.response) {
      switch (error.response.status) {
        case 401:
          message = '未授权,请重新登录';
          // 清除token并跳转登录页
          store.dispatch('resetToken');
          router.replace({
            path: '/login',
            query: { redirect: router.currentRoute.fullPath }
          });
          break;
        case 403:
          message = '拒绝访问';
          break;
        case 404:
          message = `请求地址出错: ${error.response.config.url}`;
          break;
        case 408:
          message = '请求超时';
          break;
        case 500:
          message = '服务器内部错误';
          break;
        case 501:
          message = '服务未实现';
          break;
        case 502:
          message = '网关错误';
          break;
        case 503:
          message = '服务不可用';
          break;
        case 504:
          message = '网关超时';
          break;
        case 505:
          message = 'HTTP版本不受支持';
          break;
        default:
          message = `连接出错(${error.response.status})!`;
      }
    }
    Message({
      message: message,
      type: 'error',
      duration: 5 * 1000
    });
    return Promise.reject(error);
  }
);

export default service;

frontend\src\views\Home.vue

<template>
  <div class="home-container">
    <!-- 头部导航 -->
    <div class="header">
      <div class="container">
        <div class="logo">大学生租房管理系统</div>
        <div class="nav">
          <router-link to="/home" class="nav-item active">首页</router-link>
          <router-link to="/house/map" class="nav-item">地图找房</router-link>
          <router-link to="/news" class="nav-item">租房资讯</router-link>
        </div>
        <div class="user-info">
          <template v-if="isLogin">
            <el-dropdown @command="handleCommand">
              <span class="el-dropdown-link">
                {{ username }}<i class="el-icon-arrow-down el-icon--right"></i>
              </span>
              <el-dropdown-menu slot="dropdown">
                <el-dropdown-item command="profile">个人中心</el-dropdown-item>
                <el-dropdown-item command="favorite">我的收藏</el-dropdown-item>
                <el-dropdown-item v-if="isAdmin" command="admin">管理后台</el-dropdown-item>
                <el-dropdown-item v-if="isLandlord" command="landlord">房源管理</el-dropdown-item>
                <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </template>
          <template v-else>
            <router-link to="/login" class="login-btn">登录</router-link>
            <router-link to="/register" class="register-btn">注册</router-link>
          </template>
        </div>
      </div>
    </div>

    <!-- 搜索区域 -->
    <div class="search-section">
      <div class="container">
        <div class="search-box">
          <h2>找到你心仪的房源</h2>
          <div class="search-form">
            <el-input v-model="searchForm.keyword" placeholder="请输入小区名、地址等" class="search-input">
              <el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
            </el-input>
            <div class="search-options">
              <el-select v-model="searchForm.houseType" placeholder="房型" clearable>
                <el-option label="一室" value="1"></el-option>
                <el-option label="二室" value="2"></el-option>
                <el-option label="三室" value="3"></el-option>
                <el-option label="四室及以上" value="4"></el-option>
              </el-select>
              <el-select v-model="searchForm.priceRange" placeholder="租金" clearable>
                <el-option label="1000元以下" value="0-1000"></el-option>
                <el-option label="1000-2000元" value="1000-2000"></el-option>
                <el-option label="2000-3000元" value="2000-3000"></el-option>
                <el-option label="3000-5000元" value="3000-5000"></el-option>
                <el-option label="5000元以上" value="5000-999999"></el-option>
              </el-select>
              <el-button type="primary" @click="handleSearch">搜索</el-button>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 房源列表 -->
    <div class="house-list-section">
      <div class="container">
        <div class="section-title">
          <h3>最新房源</h3>
          <router-link to="/house/list" class="more">查看更多</router-link>
        </div>
        <el-row :gutter="20">
          <el-col :span="6" v-for="(house, index) in houseList" :key="index">
            <div class="house-card" @click="goToDetail(house.id)">
              <div class="house-img">
                <img :src="house.coverUrl || 'https://via.placeholder.com/300x200'" alt="房源图片">
              </div>
              <div class="house-info">
                <div class="house-title">{{ house.title }}</div>
                <div class="house-tags">
                  <span class="tag">{{ house.houseType }}</span>
                  <span class="tag">{{ house.area }}㎡</span>
                  <span class="tag">{{ house.orientation }}</span>
                </div>
                <div class="house-address">
                  <i class="el-icon-location"></i>
                  {{ house.community }} - {{ house.address }}
                </div>
                <div class="house-price">
                  <span class="price">{{ house.price }}</span> 元/月
                </div>
              </div>
            </div>
          </el-col>
        </el-row>
        <div v-if="houseList.length === 0" class="empty-data">
          暂无房源数据
        </div>
      </div>
    </div>

    <!-- 底部区域 -->
    <div class="footer">
      <div class="container">
        <p>© 2025 大学生租房管理系统 版权所有</p>
      </div>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Home',
  data() {
    return {
      searchForm: {
        keyword: '',
        houseType: '',
        priceRange: ''
      },
      houseList: []
    }
  },
  computed: {
    ...mapGetters(['isLogin', 'username', 'isAdmin', 'isLandlord'])
  },
  created() {
    this.getHouseList()
  },
  methods: {
    async getHouseList() {
      try {
        const { data: res } = await this.$http.post('/house/search', {
          pageNum: 1,
          pageSize: 8,
          status: 1
        })
        if (res.code === 200) {
          this.houseList = res.data.list || []
        }
      } catch (error) {
        console.error(error)
        this.$message.error('获取房源列表失败')
      }
    },
    handleSearch() {
      let params = {
        keyword: this.searchForm.keyword,
        houseType: this.searchForm.houseType,
        pageNum: 1,
        pageSize: 20,
        status: 1
      }
      
      // 处理价格范围
      if (this.searchForm.priceRange) {
        const [min, max] = this.searchForm.priceRange.split('-')
        params.minPrice = min
        params.maxPrice = max
      }
      
      // 跳转到列表页并传递参数
      this.$router.push({
        path: '/house/list',
        query: params
      })
    },
    goToDetail(id) {
      this.$router.push(`/house/detail/${id}`)
    },
    handleCommand(command) {
      switch (command) {
        case 'profile':
          this.$router.push('/user/profile')
          break
        case 'favorite':
          this.$router.push('/user/favorite')
          break
        case 'admin':
          this.$router.push('/admin')
          break
        case 'landlord':
          this.$router.push('/landlord')
          break
        case 'logout':
          this.$store.dispatch('logout')
          this.$message.success('退出登录成功')
          this.$router.push('/home')
          break
      }
    }
  }
}
</script>

<style scoped>
.home-container {
  min-height: 100%;
  display: flex;
  flex-direction: column;
}

.header {
  background-color: #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
  height: 60px;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
}

.header .container {
  display: flex;
  align-items: center;
  height: 100%;
}

.logo {
  font-size: 20px;
  font-weight: bold;
  color: #409EFF;
  margin-right: 50px;
}

.nav {
  display: flex;
  flex: 1;
}

.nav-item {
  margin-right: 30px;
  color: #333;
  text-decoration: none;
  font-size: 16px;
}

.nav-item.active, .nav-item:hover {
  color: #409EFF;
}

.user-info {
  display: flex;
  align-items: center;
}

.el-dropdown-link {
  cursor: pointer;
  color: #409EFF;
}

.login-btn, .register-btn {
  margin-left: 15px;
  color: #409EFF;
  text-decoration: none;
}

.search-section {
  background-image: url('https://via.placeholder.com/1920x400');
  background-size: cover;
  background-position: center;
  height: 400px;
  margin-top: 60px;
  display: flex;
  align-items: center;
}

.search-box {
  background-color: rgba(255, 255, 255, 0.9);
  padding: 30px;
  border-radius: 5px;
  width: 100%;
}

.search-box h2 {
  text-align: center;
  margin-bottom: 20px;
  color: #333;
}

.search-input {
  margin-bottom: 15px;
}

.search-options {
  display: flex;
  justify-content: space-between;
}

.search-options .el-select {
  width: 48%;
}

.house-list-section {
  padding: 40px 0;
  flex: 1;
}

.section-title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.section-title h3 {
  font-size: 20px;
  margin: 0;
}

.more {
  color: #409EFF;
  text-decoration: none;
}

.house-card {
  background-color: #fff;
  border-radius: 5px;
  overflow: hidden;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 20px;
  cursor: pointer;
  transition: transform 0.3s;
}

.house-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.house-img {
  height: 180px;
  overflow: hidden;
}

.house-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.house-info {
  padding: 15px;
}

.house-title {
  font-size: 16px;
  font-weight: bold;
  margin-bottom: 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.house-tags {
  margin-bottom: 10px;
}

.house-tags .tag {
  display: inline-block;
  padding: 2px 6px;
  background-color: #f5f5f5;
  color: #666;
  border-radius: 2px;
  font-size: 12px;
  margin-right: 5px;
}

.house-address {
  color: #999;
  font-size: 12px;
  margin-bottom: 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.house-price {
  font-size: 14px;
  color: #666;
}

.house-price .price {
  font-size: 18px;
  color: #ff6700;
  font-weight: bold;
}

.empty-data {
  text-align: center;
  padding: 50px 0;
  color: #999;
}

.footer {
  background-color: #2d3a4b;
  color: #fff;
  padding: 20px 0;
  text-align: center;
}
</style>

frontend\src\views\Login.vue

<template>
  <div class="login-container">
    <div class="login-box">
      <div class="login-title">大学生租房管理系统</div>
      <el-form :model="loginForm" :rules="loginRules" ref="loginForm" label-width="0px">
        <el-form-item prop="username">
          <el-input v-model="loginForm.username" prefix-icon="el-icon-user" placeholder="用户名"></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input v-model="loginForm.password" prefix-icon="el-icon-lock" placeholder="密码" type="password"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" :loading="loading" @click="handleLogin" class="login-button">登录</el-button>
        </el-form-item>
        <div class="login-options">
          <span>没有账号?<router-link to="/register">立即注册</router-link></span>
          <router-link to="/home">返回首页</router-link>
        </div>
      </el-form>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Login',
  data() {
    return {
      loginForm: {
        username: '',
        password: ''
      },
      loginRules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' },
          { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
        ]
      },
      loading: false,
      redirect: undefined
    }
  },
  watch: {
    $route: {
      handler: function(route) {
        this.redirect = route.query && route.query.redirect
      },
      immediate: true
    }
  },
  methods: {
    handleLogin() {
      this.$refs.loginForm.validate(async valid => {
        if (valid) {
          this.loading = true
          try {
            const { data: res } = await this.$http.post('/user/login', this.loginForm)
            if (res.code === 200) {
              this.$message.success('登录成功')
              // 存储用户信息
              this.$store.dispatch('login', {
                token: res.data.token,
                userInfo: res.data
              })
              // 跳转到首页或重定向页面
              const path = this.redirect || '/'
              this.$router.push(path)
            } else {
              this.$message.error(res.message || '登录失败')
            }
          } catch (error) {
            console.error(error)
            this.$message.error('登录失败,请稍后再试')
          } finally {
            this.loading = false
          }
        }
      })
    }
  }
}
</script>

<style scoped>
.login-container {
  height: 100%;
  background-color: #2d3a4b;
  display: flex;
  justify-content: center;
  align-items: center;
}

.login-box {
  width: 400px;
  padding: 30px;
  background-color: #fff;
  border-radius: 5px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.login-title {
  font-size: 24px;
  font-weight: bold;
  text-align: center;
  margin-bottom: 30px;
  color: #409EFF;
}

.login-button {
  width: 100%;
}

.login-options {
  display: flex;
  justify-content: space-between;
  margin-top: 15px;
  font-size: 14px;
}

.login-options a {
  color: #409EFF;
  text-decoration: none;
}
</style>

frontend\src\views\Register.vue

<template>
  <div class="register-container">
    <div class="register-box">
      <div class="register-title">用户注册</div>
      <el-form :model="registerForm" :rules="registerRules" ref="registerForm" label-width="80px">
        <el-form-item label="用户名" prop="username">
          <el-input v-model="registerForm.username" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input v-model="registerForm.password" type="password" placeholder="请输入密码"></el-input>
        </el-form-item>
        <el-form-item label="确认密码" prop="confirmPassword">
          <el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码"></el-input>
        </el-form-item>
        <el-form-item label="手机号码" prop="phone">
          <el-input v-model="registerForm.phone" placeholder="请输入手机号码"></el-input>
        </el-form-item>
        <el-form-item label="邮箱" prop="email">
          <el-input v-model="registerForm.email" placeholder="请输入邮箱"></el-input>
        </el-form-item>
        <el-form-item label="用户类型" prop="userType">
          <el-radio-group v-model="registerForm.userType">
            <el-radio label="student">学生</el-radio>
            <el-radio label="landlord">房东</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" :loading="loading" @click="handleRegister">注册</el-button>
          <el-button @click="resetForm">重置</el-button>
        </el-form-item>
        <div class="register-options">
          <span>已有账号?<router-link to="/login">立即登录</router-link></span>
          <router-link to="/home">返回首页</router-link>
        </div>
      </el-form>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Register',
  data() {
    // 密码确认验证
    const validateConfirmPassword = (rule, value, callback) => {
      if (value !== this.registerForm.password) {
        callback(new Error('两次输入密码不一致'))
      } else {
        callback()
      }
    }
    return {
      registerForm: {
        username: '',
        password: '',
        confirmPassword: '',
        phone: '',
        email: '',
        userType: 'student'
      },
      registerRules: {
        username: [
          { required: true, message: '请输入用户名', trigger: 'blur' },
          { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
        ],
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
        ],
        confirmPassword: [
          { required: true, message: '请再次输入密码', trigger: 'blur' },
          { validator: validateConfirmPassword, trigger: 'blur' }
        ],
        phone: [
          { required: true, message: '请输入手机号码', trigger: 'blur' },
          { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
        ],
        email: [
          { required: true, message: '请输入邮箱', trigger: 'blur' },
          { type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
        ],
        userType: [
          { required: true, message: '请选择用户类型', trigger: 'change' }
        ]
      },
      loading: false
    }
  },
  methods: {
    handleRegister() {
      this.$refs.registerForm.validate(async valid => {
        if (valid) {
          this.loading = true
          try {
            const { data: res } = await this.$http.post('/user/register', {
              username: this.registerForm.username,
              password: this.registerForm.password,
              phone: this.registerForm.phone,
              email: this.registerForm.email,
              userType: this.registerForm.userType
            })
            if (res.code === 200) {
              this.$message.success('注册成功,请登录')
              this.$router.push('/login')
            } else {
              this.$message.error(res.message || '注册失败')
            }
          } catch (error) {
            console.error(error)
            this.$message.error('注册失败,请稍后再试')
          } finally {
            this.loading = false
          }
        }
      })
    },
    resetForm() {
      this.$refs.registerForm.resetFields()
    }
  }
}
</script>

<style scoped>
.register-container {
  height: 100%;
  background-color: #2d3a4b;
  display: flex;
  justify-content: center;
  align-items: center;
}

.register-box {
  width: 500px;
  padding: 30px;
  background-color: #fff;
  border-radius: 5px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.register-title {
  font-size: 24px;
  font-weight: bold;
  text-align: center;
  margin-bottom: 30px;
  color: #409EFF;
}

.register-options {
  display: flex;
  justify-content: space-between;
  margin-top: 15px;
  font-size: 14px;
}

.register-options a {
  color: #409EFF;
  text-decoration: none;
}
</style>

frontend\src\views\admin\Dashboard.vue

<template>
  <div class="dashboard-container">
    <el-row :gutter="20">
      <el-col :span="6">
        <el-card class="data-card">
          <div class="data-header">
            <div class="data-title">总用户数</div>
            <i class="el-icon-user data-icon"></i>
          </div>
          <div class="data-content">
            <div class="data-value">{{ statistics.userCount || 0 }}</div>
            <div class="data-change">
              <span :class="statistics.userIncrease > 0 ? 'increase' : 'decrease'">
                <i :class="statistics.userIncrease > 0 ? 'el-icon-top' : 'el-icon-bottom'"></i>
                {{ Math.abs(statistics.userIncrease || 0) }}%
              </span>
              较上周
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="data-card">
          <div class="data-header">
            <div class="data-title">总房源数</div>
            <i class="el-icon-house data-icon"></i>
          </div>
          <div class="data-content">
            <div class="data-value">{{ statistics.houseCount || 0 }}</div>
            <div class="data-change">
              <span :class="statistics.houseIncrease > 0 ? 'increase' : 'decrease'">
                <i :class="statistics.houseIncrease > 0 ? 'el-icon-top' : 'el-icon-bottom'"></i>
                {{ Math.abs(statistics.houseIncrease || 0) }}%
              </span>
              较上周
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="data-card">
          <div class="data-header">
            <div class="data-title">待审核认证</div>
            <i class="el-icon-document-checked data-icon"></i>
          </div>
          <div class="data-content">
            <div class="data-value">{{ statistics.pendingVerification || 0 }}</div>
            <div class="data-change">
              <router-link to="/admin/verification" class="link">查看详情</router-link>
            </div>
          </div>
        </el-card>
      </el-col>
      
      <el-col :span="6">
        <el-card class="data-card">
          <div class="data-header">
            <div class="data-title">今日访问量</div>
            <i class="el-icon-view data-icon"></i>
          </div>
          <div class="data-content">
            <div class="data-value">{{ statistics.todayVisits || 0 }}</div>
            <div class="data-change">
              <span :class="statistics.visitsIncrease > 0 ? 'increase' : 'decrease'">
                <i :class="statistics.visitsIncrease > 0 ? 'el-icon-top' : 'el-icon-bottom'"></i>
                {{ Math.abs(statistics.visitsIncrease || 0) }}%
              </span>
              较昨日
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    
    <el-row :gutter="20" class="chart-row">
      <el-col :span="12">
        <el-card class="chart-card">
          <div slot="header" class="chart-header">
            <span>用户注册统计</span>
            <el-radio-group v-model="userChartType" size="mini">
              <el-radio-button label="week">本周</el-radio-button>
              <el-radio-button label="month">本月</el-radio-button>
              <el-radio-button label="year">本年</el-radio-button>
            </el-radio-group>
          </div>
          <div class="chart-container" ref="userChart"></div>
        </el-card>
      </el-col>
      
      <el-col :span="12">
        <el-card class="chart-card">
          <div slot="header" class="chart-header">
            <span>房源发布统计</span>
            <el-radio-group v-model="houseChartType" size="mini">
              <el-radio-button label="week">本周</el-radio-button>
              <el-radio-button label="month">本月</el-radio-button>
              <el-radio-button label="year">本年</el-radio-button>
            </el-radio-group>
          </div>
          <div class="chart-container" ref="houseChart"></div>
        </el-card>
      </el-col>
    </el-row>
    
    <el-row :gutter="20">
      <el-col :span="12">
        <el-card class="table-card">
          <div slot="header" class="table-header">
            <span>最新用户</span>
            <el-button type="text" @click="$router.push('/admin/user')">查看更多</el-button>
          </div>
          <el-table :data="latestUsers" style="width: 100%" size="mini">
            <el-table-column prop="username" label="用户名" width="120"></el-table-column>
            <el-table-column prop="realName" label="真实姓名" width="120"></el-table-column>
            <el-table-column prop="roleName" label="角色"></el-table-column>
            <el-table-column prop="createTime" label="注册时间" :formatter="formatDate"></el-table-column>
          </el-table>
        </el-card>
      </el-col>
      
      <el-col :span="12">
        <el-card class="table-card">
          <div slot="header" class="table-header">
            <span>最新房源</span>
            <el-button type="text" @click="$router.push('/admin/house')">查看更多</el-button>
          </div>
          <el-table :data="latestHouses" style="width: 100%" size="mini">
            <el-table-column prop="title" label="标题" width="180"></el-table-column>
            <el-table-column prop="price" label="租金" width="100">
              <template slot-scope="scope">
                {{ scope.row.price }}元/月
              </template>
            </el-table-column>
            <el-table-column prop="statusText" label="状态">
              <template slot-scope="scope">
                <el-tag :type="getStatusType(scope.row.status)">{{ scope.row.statusText }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="createTime" label="发布时间" :formatter="formatDate"></el-table-column>
          </el-table>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script>
import { formatDate } from '@/utils/date';
import * as echarts from 'echarts';

export default {
  name: 'AdminDashboard',
  data() {
    return {
      statistics: {
        userCount: 0,
        userIncrease: 0,
        houseCount: 0,
        houseIncrease: 0,
        pendingVerification: 0,
        todayVisits: 0,
        visitsIncrease: 0
      },
      userChartType: 'week',
      houseChartType: 'week',
      latestUsers: [],
      latestHouses: [],
      userChart: null,
      houseChart: null
    };
  },
  created() {
    this.fetchStatistics();
    this.fetchLatestUsers();
    this.fetchLatestHouses();
  },
  mounted() {
    this.initCharts();
    window.addEventListener('resize', this.resizeCharts);
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resizeCharts);
    if (this.userChart) {
      this.userChart.dispose();
    }
    if (this.houseChart) {
      this.houseChart.dispose();
    }
  },
  watch: {
    userChartType() {
      this.updateUserChart();
    },
    houseChartType() {
      this.updateHouseChart();
    }
  },
  methods: {
    formatDate(row, column, cellValue) {
      return formatDate(cellValue);
    },
    getStatusType(status) {
      const typeMap = {
        0: 'info',    // 待审核
        1: 'success', // 已上架
        2: 'warning', // 已下架
        3: 'danger'   // 已出租
      };
      return typeMap[status] || 'info';
    },
    async fetchStatistics() {
      // 模拟数据,实际项目中应该从API获取
      this.statistics = {
        userCount: 256,
        userIncrease: 5.2,
        houseCount: 128,
        houseIncrease: 3.7,
        pendingVerification: 12,
        todayVisits: 1024,
        visitsIncrease: -2.1
      };
    },
    async fetchLatestUsers() {
      // 模拟数据,实际项目中应该从API获取
      this.latestUsers = [
        { id: 1, username: 'student1', realName: '张三', roleName: '学生', createTime: new Date() },
        { id: 2, username: 'landlord1', realName: '李四', roleName: '房东', createTime: new Date() },
        { id: 3, username: 'student2', realName: '王五', roleName: '学生', createTime: new Date() },
        { id: 4, username: 'landlord2', realName: '赵六', roleName: '房东', createTime: new Date() },
        { id: 5, username: 'student3', realName: '钱七', roleName: '学生', createTime: new Date() }
      ];
    },
    async fetchLatestHouses() {
      // 模拟数据,实际项目中应该从API获取
      this.latestHouses = [
        { id: 1, title: '温馨一室一厅', price: 1500, status: 1, statusText: '已上架', createTime: new Date() },
        { id: 2, title: '精装两室一厅', price: 2500, status: 1, statusText: '已上架', createTime: new Date() },
        { id: 3, title: '阳光三室两厅', price: 3500, status: 0, statusText: '待审核', createTime: new Date() },
        { id: 4, title: '豪华四室两厅', price: 4500, status: 2, statusText: '已下架', createTime: new Date() },
        { id: 5, title: '舒适单间', price: 1000, status: 3, statusText: '已出租', createTime: new Date() }
      ];
    },
    initCharts() {
      // 初始化用户注册统计图表
      this.userChart = echarts.init(this.$refs.userChart);
      this.updateUserChart();
      
      // 初始化房源发布统计图表
      this.houseChart = echarts.init(this.$refs.houseChart);
      this.updateHouseChart();
    },
    updateUserChart() {
      // 根据选择的类型生成不同的数据
      let xData = [];
      let yData = [];
      
      if (this.userChartType === 'week') {
        xData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
        yData = [5, 7, 3, 9, 12, 6, 8];
      } else if (this.userChartType === 'month') {
        xData = Array.from({ length: 30 }, (_, i) => i + 1 + '日');
        yData = Array.from({ length: 30 }, () => Math.floor(Math.random() * 10) + 1);
      } else {
        xData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
        yData = [30, 25, 35, 45, 55, 40, 35, 30, 50, 60, 45, 40];
      }
      
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: xData,
          axisTick: {
            alignWithLabel: true
          }
        },
        yAxis: {
          type: 'value'
        },
        series: [{
          name: '注册用户数',
          type: 'bar',
          barWidth: '60%',
          data: yData,
          itemStyle: {
            color: '#409EFF'
          }
        }]
      };
      
      this.userChart.setOption(option);
    },
    updateHouseChart() {
      // 根据选择的类型生成不同的数据
      let xData = [];
      let yData = [];
      
      if (this.houseChartType === 'week') {
        xData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
        yData = [3, 5, 2, 7, 9, 4, 6];
      } else if (this.houseChartType === 'month') {
        xData = Array.from({ length: 30 }, (_, i) => i + 1 + '日');
        yData = Array.from({ length: 30 }, () => Math.floor(Math.random() * 5) + 1);
      } else {
        xData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
        yData = [20, 15, 25, 30, 40, 25, 20, 15, 35, 45, 30, 25];
      }
      
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'line'
          }
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '3%',
          containLabel: true
        },
        xAxis: {
          type: 'category',
          data: xData,
          axisTick: {
            alignWithLabel: true
          }
        },
        yAxis: {
          type: 'value'
        },
        series: [{
          name: '发布房源数',
          type: 'line',
          smooth: true,
          data: yData,
          itemStyle: {
            color: '#67C23A'
          },
          areaStyle: {
            color: {
              type: 'linear',
              x: 0,
              y: 0,
              x2: 0,
              y2: 1,
              colorStops: [{
                offset: 0, color: 'rgba(103, 194, 58, 0.3)'
              }, {
                offset: 1, color: 'rgba(103, 194, 58, 0.1)'
              }]
            }
          }
        }]
      };
      
      this.houseChart.setOption(option);
    },
    resizeCharts() {
      if (this.userChart) {
        this.userChart.resize();
      }
      if (this.houseChart) {
        this.houseChart.resize();
      }
    }
  }
};
</script>

<style scoped>
.dashboard-container {
  padding: 20px;
}

.data-card {
  height: 120px;
  margin-bottom: 20px;
}

.data-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}

.data-title {
  font-size: 16px;
  color: #666;
}

.data-icon {
  font-size: 24px;
  color: #409EFF;
}

.data-content {
  display: flex;
  flex-direction: column;
}

.data-value {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 5px;
}

.data-change {
  font-size: 12px;
  color: #999;
}

.increase {
  color: #67C23A;
  margin-right: 5px;
}

.decrease {
  color: #F56C6C;
  margin-right: 5px;
}

.link {
  color: #409EFF;
  text-decoration: none;
}

.chart-row {
  margin-bottom: 20px;
}

.chart-card {
  margin-bottom: 20px;
}

.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.chart-container {
  height: 300px;
}

.table-card {
  margin-bottom: 20px;
}

.table-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>
Main Application Entry Point

frontend\src\views\appointment\AppointmentCalendar.vue

<template>
  <div class="appointment-calendar-container">
    <el-card class="calendar-card">
      <div slot="header" class="card-header">
        <span>预约日程</span>
        <div class="header-actions">
          <el-radio-group v-model="calendarView" size="small">
            <el-radio-button label="month">月视图</el-radio-button>
            <el-radio-button label="week">周视图</el-radio-button>
            <el-radio-button label="day">日视图</el-radio-button>
          </el-radio-group>
          <el-button type="primary" size="small" @click="fetchAppointments">刷新</el-button>
        </div>
      </div>
      
      <div class="calendar-container" v-loading="loading">
        <full-calendar
          ref="calendar"
          :events="calendarEvents"
          :config="calendarConfig"
          @event-selected="handleEventSelected"
          @day-click="handleDayClick">
        </full-calendar>
      </div>
    </el-card>
    
    <!-- 预约详情对话框 -->
    <el-dialog title="预约详情" :visible.sync="appointmentDialogVisible" width="600px">
      <div v-if="selectedAppointment" class="appointment-detail">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="预约编号">{{ selectedAppointment.id }}</el-descriptions-item>
          <el-descriptions-item label="预约状态">
            <el-tag :type="getStatusType(selectedAppointment.status)">{{ getStatusText(selectedAppointment.status) }}</el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="预约时间">{{ formatDate(selectedAppointment.appointmentTime) }}</el-descriptions-item>
          <el-descriptions-item label="创建时间">{{ formatDate(selectedAppointment.createTime) }}</el-descriptions-item>
          <el-descriptions-item label="联系人">{{ selectedAppointment.contactName }}</el-descriptions-item>
          <el-descriptions-item label="联系电话">{{ selectedAppointment.contactPhone }}</el-descriptions-item>
          <el-descriptions-item label="预约备注" :span="2">
            {{ selectedAppointment.appointmentNotes || '无' }}
          </el-descriptions-item>
        </el-descriptions>
        
        <div class="dialog-actions">
          <el-button
            v-if="selectedAppointment.status === 0"
            type="danger"
            @click="handleCancel(selectedAppointment)">取消预约</el-button>
          <el-button
            v-if="isLandlord && selectedAppointment.status === 0"
            type="primary"
            @click="handleConfirm(selectedAppointment)">确认预约</el-button>
          <el-button
            v-if="isLandlord && selectedAppointment.status === 1"
            type="success"
            @click="handleComplete(selectedAppointment)">完成预约</el-button>
          <el-button
            type="info"
            @click="viewAppointmentDetail(selectedAppointment)">查看详情</el-button>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import FullCalendar from 'vue-fullcalendar';
import appointmentApi from '@/api/appointment';
import { formatDate } from '@/utils/date';
import { mapGetters } from 'vuex';

export default {
  name: 'AppointmentCalendar',
  components: {
    FullCalendar
  },
  data() {
    return {
      loading: false,
      calendarView: 'month',
      appointments: [],
      selectedAppointment: null,
      appointmentDialogVisible: false,
      calendarConfig: {
        header: {
          left: 'prev,next today',
          center: 'title',
          right: ''
        },
        defaultView: 'month',
        locale: 'zh-cn',
        buttonText: {
          today: '今天',
          month: '月',
          week: '周',
          day: '日'
        },
        timeFormat: 'HH:mm',
        eventLimit: true,
        eventLimitText: '更多',
        firstDay: 1, // 周一开始
        height: 'auto'
      }
    };
  },
  computed: {
    ...mapGetters(['userId', 'userInfo']),
    isLandlord() {
      return this.selectedAppointment && this.userInfo && this.selectedAppointment.landlordId === this.userId;
    },
    calendarEvents() {
      return this.appointments.map(appointment => {
        const statusColorMap = {
          0: '#909399', // 待确认 - 灰色
          1: '#E6A23C', // 已确认 - 黄色
          2: '#F56C6C', // 已取消 - 红色
          3: '#67C23A'  // 已完成 - 绿色
        };
        
        return {
          id: appointment.id,
          title: this.getEventTitle(appointment),
          start: appointment.appointmentTime,
          end: this.getEndTime(appointment.appointmentTime),
          backgroundColor: statusColorMap[appointment.status],
          borderColor: statusColorMap[appointment.status],
          extendedProps: appointment
        };
      });
    }
  },
  created() {
    this.fetchAppointments();
  },
  watch: {
    calendarView(val) {
      if (this.$refs.calendar) {
        this.$refs.calendar.fireMethod('changeView', val);
      }
    }
  },
  methods: {
    formatDate,
    getStatusType(status) {
      const typeMap = {
        0: 'info',    // 待确认
        1: 'warning', // 已确认
        2: 'danger',  // 已取消
        3: 'success'  // 已完成
      };
      return typeMap[status] || 'info';
    },
    getStatusText(status) {
      const textMap = {
        0: '待确认',
        1: '已确认',
        2: '已取消',
        3: '已完成'
      };
      return textMap[status] || '未知';
    },
    getEventTitle(appointment) {
      const statusText = this.getStatusText(appointment.status);
      return `[${statusText}] ${appointment.contactName}`;
    },
    getEndTime(startTime) {
      const end = new Date(startTime);
      end.setHours(end.getHours() + 1); // 默认预约时长1小时
      return end;
    },
    fetchAppointments() {
      this.loading = true;
      
      // 获取当前日历视图的日期范围
      let startDate, endDate;
      if (this.$refs.calendar) {
        const view = this.$refs.calendar.fireMethod('getView');
        startDate = view.start.format('YYYY-MM-DD');
        endDate = view.end.format('YYYY-MM-DD');
      } else {
        // 默认获取当前月
        const now = new Date();
        const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
        const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
        startDate = formatDate(firstDay, 'yyyy-MM-dd');
        endDate = formatDate(lastDay, 'yyyy-MM-dd');
      }
      
      appointmentApi.getAppointmentsByDateRange(startDate, endDate)
        .then(response => {
          this.appointments = response.data || [];
        })
        .catch(error => {
          this.$message.error(error.message || '获取预约列表失败');
        })
        .finally(() => {
          this.loading = false;
        });
    },
    handleEventSelected(event) {
      this.selectedAppointment = event.extendedProps;
      this.appointmentDialogVisible = true;
    },
    handleDayClick(date) {
      // 可以在这里实现点击日期创建新预约的功能
      console.log('Day clicked:', date);
    },
    handleCancel(appointment) {
      this.$confirm('确定要取消此预约吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        appointmentApi.cancelAppointment(appointment.id)
          .then(() => {
            this.$message.success('预约已取消');
            this.appointmentDialogVisible = false;
            this.fetchAppointments();
          })
          .catch(error => {
            this.$message.error(error.message || '取消预约失败');
          });
      }).catch(() => {
        // 取消操作
      });
    },
    handleConfirm(appointment) {
      this.$confirm('确定要确认此预约吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'info'
      }).then(() => {
        appointmentApi.confirmAppointment(appointment.id)
          .then(() => {
            this.$message.success('预约已确认');
            this.appointmentDialogVisible = false;
            this.fetchAppointments();
          })
          .catch(error => {
            this.$message.error(error.message || '确认预约失败');
          });
      }).catch(() => {
        // 取消操作
      });
    },
    handleComplete(appointment) {
      this.$confirm('确定要完成此预约吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'info'
      }).then(() => {
        appointmentApi.completeAppointment(appointment.id)
          .then(() => {
            this.$message.success('预约已完成');
            this.appointmentDialogVisible = false;
            this.fetchAppointments();
          })
          .catch(error => {
            this.$message.error(error.message || '完成预约失败');
          });
      }).catch(() => {
        // 取消操作
      });
    },
    viewAppointmentDetail(appointment) {
      this.appointmentDialogVisible = false;
      this.$router.push(`/appointment/detail/${appointment.id}`);
    }
  }
};
</script>

<style scoped>
.appointment-calendar-container {
  padding: 20px;
}

.calendar-card {
  margin-bottom: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.calendar-container {
  min-height: 600px;
}

.appointment-detail {
  margin-bottom: 20px;
}

.dialog-actions {
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>

frontend\src\views\appointment\AppointmentDetail.vue

<template>
  <div class="appointment-detail-container">
    <el-card class="appointment-card" v-loading="loading">
      <div slot="header" class="card-header">
        <span>预约详情</span>
        <el-button icon="el-icon-back" @click="goBack">返回</el-button>
      </div>
      
      <div v-if="appointment" class="appointment-info">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="预约编号">{{ appointment.id }}</el-descriptions-item>
          <el-descriptions-item label="预约状态">
            <el-tag :type="getStatusType(appointment.status)">{{ getStatusText(appointment.status) }}</el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="预约时间">{{ formatDate(appointment.appointmentTime) }}</el-descriptions-item>
          <el-descriptions-item label="创建时间">{{ formatDate(appointment.createTime) }}</el-descriptions-item>
          <el-descriptions-item label="联系人">{{ appointment.contactName }}</el-descriptions-item>
          <el-descriptions-item label="联系电话">{{ appointment.contactPhone }}</el-descriptions-item>
          <el-descriptions-item label="预约备注" :span="2">
            {{ appointment.appointmentNotes || '无' }}
          </el-descriptions-item>
        </el-descriptions>
      </div>
      
      <div v-if="house" class="house-info">
        <h3>房源信息</h3>
        <el
The model produced an invalid tool call, trying again.

No credits consumed on this tool call.

Erroran internal error occurred (error ID: 0bf2dd709b094428b66199d732f6548e)

DoneFeedback has been submitted
Me

补全不完整的代码, 及遗漏的文件。

Let me complete the unfinished AppointmentDetail.vue file and provide the remaining frontend components for the appointment module:

Completing the AppointmentDetail.vue file

frontend\src\views\appointment\AppointmentDetail.vue (continuation)

<el-card class="house-card" shadow="hover">
          <div class="house-title">{{ house.title }}</div>
          <div class="house-info-grid">
            <div class="info-item">
              <span class="label">价格:</span>
              <span class="value">{{ house.price }}元/月</span>
            </div>
            <div class="info-item">
              <span class="label">面积:</span>
              <span class="value">{{ house.area }}㎡</span>
            </div>
            <div class="info-item">
              <span class="label">户型:</span>
              <span class="value">{{ house.houseType }}</span>
            </div>
            <div class="info-item">
              <span class="label">朝向:</span>
              <span class="value">{{ house.orientation }}</span>
            </div>
            <div class="info-item">
              <span class="label">楼层:</span>
              <span class="value">{{ house.floor }}</span>
            </div>
            <div class="info-item">
              <span class="label">装修:</span>
              <span class="value">{{ house.decoration }}</span>
            </div>
          </div>
          <div class="house-address">
            <span class="label">地址:</span>
            <span class="value">{{ house.community }} {{ house.address }}</span>
          </div>
          <div class="house-contact">
            <span class="label">联系人:</span>
            <span class="value">{{ house.contact }} {{ house.contactPhone }}</span>
          </div>
        </el-card>
      </div>
      
      <div v-if="feedback" class="feedback-info">
        <h3>看房反馈</h3>
        <el-card class="feedback-card" shadow="hover">
          <div class="feedback-header">
            <el-rate
              v-model="feedback.satisfactionLevel"
              disabled
              show-score
              text-color="#ff9900">
            </el-rate>
            <span class="feedback-time">{{ formatDate(feedback.createTime) }}</span>
          </div>
          <div class="feedback-content">{{ feedback.feedbackContent }}</div>
        </el-card>
      </div>
      
      <div class="action-buttons" v-if="appointment">
        <el-button
          v-if="appointment.status === 0"
          type="danger"
          @click="handleCancel">取消预约</el-button>
        <el-button
          v-if="isLandlord && appointment.status === 0"
          type="primary"
          @click="handleConfirm">确认预约</el-button>
        <el-button
          v-if="isLandlord && appointment.status === 1"
          type="success"
          @click="handleComplete">完成预约</el-button>
        <el-button
          v-if="appointment.status === 3 && !feedback"
          type="primary"
          @click="handleFeedback">提交反馈</el-button>
      </div>
      
      <!-- 反馈表单对话框 -->
      <el-dialog title="看房反馈" :visible.sync="feedbackDialogVisible" width="600px">
        <el-form ref="feedbackForm" :model="feedbackForm" :rules="feedbackRules" label-width="100px">
          <el-form-item label="满意度" prop="satisfactionLevel">
            <el-rate
              v-model="feedbackForm.satisfactionLevel"
              :colors="['#99A9BF', '#F7BA2A', '#FF9900']"
              :texts="['失望', '一般', '满意', '很满意', '非常满意']"
              show-text>
            </el-rate>
          </el-form-item>
          <el-form-item label="反馈内容" prop="feedbackContent">
            <el-input
              v-model="feedbackForm.feedbackContent"
              type="textarea"
              :rows="6"
              placeholder="请详细描述您的看房体验,包括房源状况、交通便利性、周边环境等">
            </el-input>
          </el-form-item>
          <el-form-item label="是否公开">
            <el-switch
              v-model="feedbackForm.isPublic"
              :active-value="1"
              :inactive-value="0"
              active-text="公开"
              inactive-text="私密">
            </el-switch>
          </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
          <el-button @click="feedbackDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitFeedback" :loading="feedbackLoading">提交</el-button>
        </div>
      </el-dialog>
    </el-card>
  </div>
</template>

<script>
import appointmentApi from '@/api/appointment';
import feedbackApi from '@/api/feedback';
import houseApi from '@/api/house';
import { formatDate } from '@/utils/date';
import { mapGetters } from 'vuex';

export default {
  name: 'AppointmentDetail',
  data() {
    return {
      loading: false,
      appointment: null,
      house: null,
      feedback: null,
      
      // 反馈相关
      feedbackDialogVisible: false,
      feedbackLoading: false,
      feedbackForm: {
        appointmentId: null,
        satisfactionLevel: 3,
        feedbackContent: '',
        isPublic: 1
      },
      feedbackRules: {
        satisfactionLevel: [
          { required: true, message: '请选择满意度', trigger: 'change' }
        ],
        feedbackContent: [
          { required: true, message: '请输入反馈内容', trigger: 'blur' },
          { min: 10, max: 500, message: '长度在 10 到 500 个字符', trigger: 'blur' }
        ]
      }
    };
  },
  computed: {
    ...mapGetters(['userId', 'userInfo']),
    appointmentId() {
      return this.$route.params.id;
    },
    isLandlord() {
      return this.appointment && this.userInfo && this.appointment.landlordId === this.userId;
    }
  },
  created() {
    this.fetchAppointmentDetail();
  },
  methods: {
    formatDate,
    getStatusType(status) {
      const typeMap = {
        0: 'info',    // 待确认
        1: 'warning', // 已确认
        2: 'danger',  // 已取消
        3: 'success'  // 已完成
      };
      return typeMap[status] || 'info';
    },
    getStatusText(status) {
      const textMap = {
        0: '待确认',
        1: '已确认',
        2: '已取消',
        3: '已完成'
      };
      return textMap[status] || '未知';
    },
    fetchAppointmentDetail() {
      this.loading = true;
      
      appointmentApi.getAppointmentDetail(this.appointmentId)
        .then(response => {
          this.appointment = response.data;
          this.fetchHouseInfo();
          this.fetchFeedback();
        })
        .catch(error => {
          this.$message.error(error.message || '获取预约详情失败');
        })
        .finally(() => {
          this.loading = false;
        });
    },
    fetchHouseInfo() {
      if (!this.appointment) return;
      
      houseApi.getHouseDetail(this.appointment.houseId)
        .then(response => {
          this.house = response.data;
        })
        .catch(error => {
          this.$message.error(error.message || '获取房源信息失败');
        });
    },
    fetchFeedback() {
      if (!this.appointment) return;
      
      feedbackApi.getAppointmentFeedback(this.appointment.id)
        .then(response => {
          if (response.code === 200) {
            this.feedback = response.data;
          }
        })
        .catch(() => {
          // 忽略错误
        });
    },
    goBack() {
      this.$router.go(-1);
    },
    handleCancel() {
      this.$confirm('确定要取消此预约吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        appointmentApi.cancelAppointment(this.appointment.id)
          .then(() => {
            this.$message.success('预约已取消');
            this.fetchAppointmentDetail();
          })
          .catch(error => {
            this.$message.error(error.message || '取消预约失败');
          });
      }).catch(() => {
        // 取消操作
      });
    },
    handleConfirm() {
      this.$confirm('确定要确认此预约吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'info'
      }).then(() => {
        appointmentApi.confirmAppointment(this.appointment.id)
          .then(() => {
            this.$message.success('预约已确认');
            this.fetchAppointmentDetail();
          })
          .catch(error => {
            this.$message.error(error.message || '确认预约失败');
          });
      }).catch(() => {
        // 取消操作
      });
    },
    handleComplete() {
      this.$confirm('确定要完成此预约吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'info'
      }).then(() => {
        appointmentApi.completeAppointment(this.appointment.id)
          .then(() => {
            this.$message.success('预约已完成');
            this.fetchAppointmentDetail();
          })
          .catch(error => {
            this.$message.error(error.message || '完成预约失败');
          });
      }).catch(() => {
        // 取消操作
      });
    },
    handleFeedback() {
      this.feedbackForm.appointmentId = this.appointment.id;
      this.feedbackForm.satisfactionLevel = 3;
      this.feedbackForm.feedbackContent = '';
      this.feedbackForm.isPublic = 1;
      this.feedbackDialogVisible = true;
    },
    submitFeedback() {
      this.$refs.feedbackForm.validate(valid => {
        if (valid) {
          this.feedbackLoading = true;
          feedbackApi.submitFeedback(this.feedbackForm)
            .then(() => {
              this.$message.success('反馈提交成功');
              this.feedbackDialogVisible = false;
              this.fetchFeedback();
            })
            .catch(error => {
              this.$message.error(error.message || '反馈提交失败');
            })
            .finally(() => {
              this.feedbackLoading = false;
            });
        } else {
          return false;
        }
      });
    }
  }
};
</script>

<style scoped>
.appointment-detail-container {
  padding: 20px;
}

.appointment-card {
  margin-bottom: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.appointment-info {
  margin-bottom: 30px;
}

.house-info {
  margin-bottom: 30px;
}

.house-card {
  padding: 15px;
}

.house-title {
  font-size: 18px;
  font-weight: bold;
  margin-bottom: 15px;
}

.house-info-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 15px;
  margin-bottom: 15px;
}

.info-item {
  display: flex;
}

.label {
  color: #909399;
  margin-right: 5px;
}

.house-address, .house-contact {
  margin-top: 10px;
}

.feedback-info {
  margin-bottom: 30px;
}

.feedback-card {
  padding: 15px;
}

.feedback-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
}

.feedback-time {
  color: #909399;
  font-size: 14px;
}

.feedback-content {
  line-height: 1.6;
}

.action-buttons {
  margin-top: 20px;
}
</style>

frontend\src\views\appointment\AppointmentForm.vue

<template>
  <div class="appointment-form-container">
    <el-card class="appointment-card">
      <div slot="header" class="card-header">
        <span>预约看房</span>
      </div>
      <el-form ref="appointmentForm" :model="appointmentForm" :rules="rules" label-width="100px">
        <el-form-item label="联系人" prop="contactName">
          <el-input v-model="appointmentForm.contactName" placeholder="请输入联系人姓名"></el-input>
        </el-form-item>
        <el-form-item label="联系电话" prop="contactPhone">
          <el-input v-model="appointmentForm.contactPhone" placeholder="请输入联系电话"></el-input>
        </el-form-item>
        <el-form-item label="预约时间" prop="appointmentTime">
          <el-date-picker
            v-model="appointmentForm.appointmentTime"
            type="datetime"
            placeholder="选择预约时间"
            :picker-options="pickerOptions"
            value-format="yyyy-MM-dd HH:mm:ss"
            format="yyyy-MM-dd HH:mm">
          </el-date-picker>
        </el-form-item>
        <el-form-item label="预约备注" prop="appointmentNotes">
          <el-input
            v-model="appointmentForm.appointmentNotes"
            type="textarea"
            :rows="4"
            placeholder="请输入预约备注,如特殊要求等">
          </el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="submitAppointment" :loading="loading">提交预约</el-button>
          <el-button @click="resetForm">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script>
import appointmentApi from '@/api/appointment';
import { formatDate } from '@/utils/date';

export default {
  name: 'AppointmentForm',
  props: {
    houseId: {
      type: [Number, String],
      required: true
    }
  },
  data() {
    // 验证手机号
    const validatePhone = (rule, value, callback) => {
      if (!/^1[3-9]\d{9}$/.test(value)) {
        callback(new Error('请输入正确的手机号码'));
      } else {
        callback();
      }
    };
    
    // 验证预约时间
    const validateAppointmentTime = (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择预约时间'));
        return;
      }
      
      const now = new Date();
      const appointmentTime = new Date(value);
      
      if (appointmentTime <= now) {
        callback(new Error('预约时间必须是将来的时间'));
        return;
      }
      
      callback();
    };
    
    return {
      appointmentForm: {
        houseId: this.houseId,
        contactName: '',
        contactPhone: '',
        appointmentTime: '',
        appointmentNotes: ''
      },
      rules: {
        contactName: [
          { required: true, message: '请输入联系人姓名', trigger: 'blur' },
          { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
        ],
        contactPhone: [
          { required: true, message: '请输入联系电话', trigger: 'blur' },
          { validator: validatePhone, trigger: 'blur' }
        ],
        appointmentTime: [
          { required: true, message: '请选择预约时间', trigger: 'change' },
          { validator: validateAppointmentTime, trigger: 'change' }
        ]
      },
      pickerOptions: {
        disabledDate(time) {
          return time.getTime() < Date.now() - 8.64e7; // 禁用今天之前的日期
        },
        selectableRange: '09:00:00 - 18:00:00' // 可选时间范围
      },
      loading: false
    };
  },
  methods: {
    submitAppointment() {
      this.$refs.appointmentForm.validate(valid => {
        if (valid) {
          this.loading = true;
          appointmentApi.createAppointment(this.appointmentForm)
            .then(response => {
              this.$message.success('预约提交成功,请等待房东确认');
              this.$emit('success', response.data);
              this.resetForm();
            })
            .catch(error => {
              this.$message.error(error.message || '预约提交失败,请稍后重试');
            })
            .finally(() => {
              this.loading = false;
            });
        } else {
          return false;
        }
      });
    },
    resetForm() {
      this.$refs.appointmentForm.resetFields();
    }
  }
};
</script>

<style scoped>
.appointment-form-container {
  max-width: 600px;
  margin: 0 auto;
}

.appointment-card {
  margin-bottom: 20px;
}

.card-header {
  font-size: 18px;
  font-weight: bold;
}
</style>

frontend\src\views\appointment\AppointmentList.vue

<template>
  <div class="appointment-list-container">
    <el-card class="appointment-card">
      <div slot="header" class="card-header">
        <span>我的预约</span>
        <el-radio-group v-model="activeTab" size="small" @change="handleTabChange">
          <el-radio-button label="all">全部</el-radio-button>
          <el-radio-button label="pending">待确认</el-radio-button>
          <el-radio-button label="confirmed">已确认</el-radio-button>
          <el-radio-button label="completed">已完成</el-radio-button>
          <el-radio-button label="canceled">已取消</el-radio-button>
        </el-radio-group>
      </div>
      
      <div class="filter-container">
        <el-date-picker
          v-model="dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="yyyy-MM-dd"
          @change="handleDateRangeChange">
        </el-date-picker>
        <el-button type="primary" icon="el-icon-search" @click="fetchAppointments">查询</el-button>
      </div>
      
      <el-table
        v-loading="loading"
        :data="appointmentList"
        style="width: 100%"
        @row-click="handleRowClick">
        <el-table-column prop="id" label="预约编号" width="100"></el-table-column>
        <el-table-column label="房源信息" min-width="200">
          <template slot-scope="scope">
            <div v-if="scope.row.house">{{ scope.row.house.title }}</div>
            <div v-else>房源信息加载中...</div>
          </template>
        </el-table-column>
        <el-table-column label="预约时间" width="180">
          <template slot-scope="scope">
            {{ formatDate(scope.row.appointmentTime) }}
          </template>
        </el-table-column>
        <el-table-column label="联系人" width="120">
          <template slot-scope="scope">
            {{ scope.row.contactName }}<br>
            {{ scope.row.contactPhone }}
          </template>
        </el-table-column>
        <el-table-column label="状态" width="100">
          <template slot-scope="scope">
            <el-tag :type="getStatusType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200">
          <template slot-scope="scope">
            <el-button
              v-if="scope.row.status === 0"
              size="mini"
              type="danger"
              @click.stop="handleCancel(scope.row)">取消预约</el-button>
            <el-button
              v-if="scope.row.status === 3 && !scope.row.hasFeedback"
              size="mini"
              type="primary"
              @click.stop="handleFeedback(scope.row)">提交反馈</el-button>
            <el-button
              size="mini"
              type="info"
              @click.stop="handleDetail(scope.row)">查看详情</el-button>
          </template>
        </el-table-column>
      </el-table>
      
      <div class="pagination-container">
        <el-pagination
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="currentPage"
          :page-sizes="[10, 20, 50, 100]"
          :page-size="pageSize"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total">
        </el-pagination>
      </div>
    </el-card>
    
    <!-- 反馈表单对话框 -->
    <el-dialog title="看房反馈" :visible.sync="feedbackDialogVisible" width="600px">
      <el-form ref="feedbackForm" :model="feedbackForm" :rules="feedbackRules" label-width="100px">
        <el-form-item label="满意度" prop="satisfactionLevel">
          <el-rate
            v-model="feedbackForm.satisfactionLevel"
            :colors="['#99A9BF', '#F7BA2A', '#FF9900']"
            :texts="['失望', '一般', '满意', '很满意', '非常满意']"
            show-text>
          </el-rate>
        </el-form-item>
        <el-form-item label="反馈内容" prop="feedbackContent">
          <el-input
            v-model="feedbackForm.feedbackContent"
            type="textarea"
            :rows="6"
            placeholder="请详细描述您的看房体验,包括房源状况、交通便利性、周边环境等">
          </el-input>
        </el-form-item>
        <el-form-item label="是否公开">
          <el-switch
            v-model="feedbackForm.isPublic"
            :active-value="1"
            :inactive-value="0"
            active-text="公开"
            inactive-text="私密">
          </el-switch>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="feedbackDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitFeedback" :loading="feedbackLoading">提交</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import appointmentApi from '@/api/appointment';
import feedbackApi from '@/api/feedback';
import houseApi from '@/api/house';
import { formatDate } from '@/utils/date';

export default {
  name: 'AppointmentList',
  data() {
    return {
      activeTab: 'all',
      appointmentList: [],
      loading: false,
      currentPage: 1,
      pageSize: 10,
      total: 0,
      dateRange: null,
      
      // 反馈相关
      feedbackDialogVisible: false,
      feedbackLoading: false,
      currentAppointment: null,
      feedbackForm: {
        appointmentId: null,
        satisfactionLevel: 3,
        feedbackContent: '',
        isPublic: 1
      },
      feedbackRules: {
        satisfactionLevel: [
          { required: true, message: '请选择满意度', trigger: 'change' }
        ],
        feedbackContent: [
          { required: true, message: '请输入反馈内容', trigger: 'blur' },
          { min: 10, max: 500, message: '长度在 10 到 500 个字符', trigger: 'blur' }
        ]
      }
    };
  },
  created() {
    this.fetchAppointments();
  },
  methods: {
    formatDate,
    getStatusType(status) {
      const typeMap = {
        0: 'info',    // 待确认
        1: 'warning', // 已确认
        2: 'danger',  // 已取消
        3: 'success'  // 已完成
      };
      return typeMap[status] || 'info';
    },
    getStatusText(status) {
      const textMap = {
        0: '待确认',
        1: '已确认',
        2: '已取消',
        3: '已完成'
      };
      return textMap[status] || '未知';
    },
    fetchAppointments() {
      this.loading = true;
      
      let promise;
      if (this.dateRange && this.dateRange.length === 2) {
        promise = appointmentApi.getAppointmentsByDateRange(this.dateRange[0], this.dateRange[1]);
      } else {
        promise = appointmentApi.getUserAppointments();
      }
      
      promise.then(response => {
        let appointments = response.data || [];
        
        // 根据标签过滤
        if (this.activeTab !== 'all') {
          const statusMap = {
            'pending': 0,
            'confirmed': 1,
            'canceled': 2,
            'completed': 3
          };
          appointments = appointments.filter(item => item.status === statusMap[this.activeTab]);
        }
        
        this.total = appointments.length;
        
        // 分页处理
        const start = (this.currentPage - 1) * this.pageSize;
        const end = start + this.pageSize;
        this.appointmentList = appointments.slice(start, end);
        
        // 加载房源信息
        this.loadHouseInfo();
        
        // 检查是否已提交反馈
        this.checkFeedback();
      }).catch(error => {
        this.$message.error(error.message || '获取预约列表失败');
      }).finally(() => {
        this.loading = false;
      });
    },
    loadHouseInfo() {
      const houseIds = this.appointmentList.map(item => item.houseId);
      
      houseIds.forEach(houseId => {
        houseApi.getHouseDetail(houseId).then(response => {
          const house = response.data;
          this.appointmentList.forEach(item => {
            if (item.houseId === houseId) {
              this.$set(item, 'house', house);
            }
          });
        }).catch(() => {
          // 忽略错误
        });
      });
    },
    checkFeedback() {
      const completedAppointments = this.appointmentList.filter(item => item.status === 3);
      
      completedAppointments.forEach(appointment => {
        feedbackApi.getAppointmentFeedback(appointment.id).then(response => {
          if (response.code === 200) {
            this.$set(appointment, 'hasFeedback', true);
          } else {
            this.$set(appointment, 'hasFeedback', false);
          }
        }).catch(() => {
          this.$set(appointment, 'hasFeedback', false);
        });
      });
    },
    handleTabChange() {
      this.currentPage = 1;
      this.fetchAppointments();
    },
    handleDateRangeChange() {
      this.currentPage = 1;
    },
    handleSizeChange(val) {
      this.pageSize = val;
      this.fetchAppointments();
    },
    handleCurrentChange(val) {
      this.currentPage = val;
      this.fetchAppointments();
    },
    handleRowClick(row) {
      this.handleDetail(row);
    },
    handleDetail(row) {
      this.$router.push(`/appointment/detail/${row.id}`);
    },
    handleCancel(row) {
      this.$confirm('确定要取消此预约吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        appointmentApi.cancelAppointment(row.id).then(() => {
          this.$message.success('预约已取消');
          this.fetchAppointments();
        }).catch(error => {
          this.$message.error(error.message || '取消预约失败');
        });
      }).catch(() => {
        // 取消操作
      });
    },
    handleFeedback(row) {
      this.currentAppointment = row;
      this.feedbackForm.appointmentId = row.id;
      this.feedbackForm.satisfactionLevel = 3;
      this.feedbackForm.feedbackContent = '';
      this.feedbackForm.isPublic = 1;
      this.feedbackDialogVisible = true;
    },
    submitFeedback() {
      this.$refs.feedbackForm.validate(valid => {
        if (valid) {
          this.feedbackLoading = true;
          feedbackApi.submitFeedback(this.feedbackForm).then(() => {
            this.$message.success('反馈提交成功');
            this.feedbackDialogVisible = false;
            this.fetchAppointments();
          }).catch(error => {
            this.$message.error(error.message || '反馈提交失败');
          }).finally(() => {
            this.feedbackLoading = false;
          });
        } else {
          return false;
        }
      });
    }
  }
};
</script>

<style scoped>
.appointment-list-container {
  padding: 20px;
}

.appointment-card {
  margin-bottom: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.filter-container {
  margin-bottom: 20px;
}

.pagination-container {
  margin-top: 20px;
  text-align: right;
}
</style>

frontend\src\views\house\Detail.vue

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值