| 技术 | 版本 | 用途 |
|---|---|---|
| Vue 3 | ^3.5.13 | 前端框架(Composition API + <script setup>) |
| Vite | ^6.3.5 | 构建工具(HMR、代理配置) |
| TypeScript | ~5.8.3 | 类型安全(strict: true, noUnusedLocals: true) |
| Element Plus | ^2.13.6 | UI 组件库(主题色 #e34889) |
| Pinia | ^3.0.4 | 状态管理(Composition API 风格) |
| Pinia Persistedstate | ^4.7.1 | Store 自动持久化到 localStorage |
| Vue Router | ^4.6.4 | 路由管理(嵌套路由 + 导航守卫) |
| Axios | ^1.15.2 | HTTP 请求(withCredentials: true, 响应拦截) |
| Orval | 7.0 | OpenAPI → TypeScript API 代码生成 |
| Prettier | ^3.8.3 | 代码格式化 |
ui/src/
├── main.ts # 入口文件(createApp, use pinia/router/element-plus)
├── App.vue # 根组件(仅 <router-view/>)
├── style.css # 全局样式(CSS 变量、主题色定义)
├── vite-env.d.ts # Vite 环境类型声明
│
├── type/
│ └── index.ts # 自定义类型定义
│ - User { id, name, avatar?, role }
│ - LoginUser { isLogin, token?, user? }
│ - Pet { id, name, breed, age, weight, gender, notes? }
│ - Package { id, type, name, price, description?, durationDays?, isActive }
│ - BoardingRoom { id, name, capacity, pricePerNight, description? }
│ - RoomStatus { date, status: 'AVAILABLE'|'OCCUPIED' }
│ - Appointment { id, userId, petId, packageId, roomId?, type, startDate?,
│ endDate?, appointmentTime?, status, deposit, depositPaid,
│ totalPrice, cancelReason?, rejectReason?, createdAt }
│ - Schedule { id, designerName, workDate, timeSlot, isAvailable }
│
├── util/
│ ├── index.ts # 工具函数(当前为空)
│ └── axios-instance.ts # Axios 实例配置
│ - baseURL: '/api'(Vite 代理 /api → localhost:8080)
│ - withCredentials: true(自动携带 Cookie/JSESSIONID)
│ - 响应拦截器:校验 code !== 0/200 → reject;401 → window.location.href='/login'
│ - customAxiosInstance<T>(config): Promise<T>(供 Orval 使用)
│
├── api/ # Orval 生成的 API 控制器(按标签拆分)
│ ├── auth-controller.ts # getAuthController() → { login, register, logout }
│ ├── user-controller.ts # getUserController() → { queryByPage, add, edit, delete, ... }
│ ├── pet-controller.ts # getPetController() → { list, add, getById, update, delete, ... }
│ ├── appointment-controller.ts # getAppointmentController() → { create, list, getById, cancel, payDeposit }
│ ├── admin-appointment-controller.ts # getAdminAppointmentController() → { listAll, review, handleCancel }
│ ├── boarding-room-controller.ts # getBoardingRoomController() → { listRooms, getStatus }
│ ├── admin-boarding-room-controller.ts # getAdminBoardingRoomController() → { updateStatus, ... }
│ ├── package-controller.ts # getPackageController() → { list, getById }
│ ├── admin-package-controller.ts # getAdminPackageController() → { create, update, delete }
│ ├── admin-schedule-controller.ts # getAdminScheduleController() → { list, create, delete }
│ ├── admin-user-controller.ts # getAdminUserController() → { list, getDetail }
│ ├── schedule-controller.ts # getScheduleController()(预留)
│ └── models/ # Orval 生成的 TypeScript 类型
│ ├── index.ts # 统一导出
│ ├── response.ts # Response<T> { code, message, data }
│ ├── responseUserVo.ts, responseBoolean.ts, ...
│ ├── userVo.ts, userDto.ts, loginDto.ts, registerDto.ts
│ ├── updateUserPasswordDto.ts, updateUserAvatarDto.ts, updateUserStatusDto.ts
│ ├── deleteByIdParams.ts, queryByPageParams.ts, pageVoListUserVo.ts
│ ├── appointmentDto.ts, reviewDto.ts, handleCancelDto.ts
│ └── ...
│
├── store/ # Pinia 状态管理
│ ├── index.ts # 统一导出 { useLoginUserStore, useMetaStore, useAppointmentStore, usePetStore }
│ ├── user.ts # useLoginUserStore — 用户登录状态
│ ├── meta.ts # useMetaStore — 站点元数据
│ ├── appointment.ts # useAppointmentStore — 预约流程临时状态
│ └── pet.ts # usePetStore — 宠物列表缓存
│
├── router/
│ └── index.ts # Vue Router 配置 + 导航守卫
│ - 公共路由: /login
│ - 用户侧: UserLayout + children(预约/订单/宠物/个人中心)
│ - 管理侧: AdminLayout + children(订单审核/房态/排班/套餐/用户)
│ - beforeEach 守卫: requiresAuth → 登录校验, requiresAdmin → 角色校验
│
├── layout/
│ ├── UserLayout.vue # 用户侧布局(水平 TopNav + 内容区)
│ └── AdminLayout.vue # 商家后台侧布局(可折叠侧边栏 + 顶栏 + 内容区)
│
├── view/
│ ├── HomeView.vue # 首页(品牌展示,快速入口卡片)
│ ├── LoginView.vue # 登录/注册(Tab 切换,毛玻璃卡片,渐变背景)
│ ├── AboutView.vue # 关于页面(占位)
│ ├── UserView.vue # 用户管理(后台,搜索/表格/CRUD/分页/状态切换)
│ │
│ ├── booking/ # 服务预约模块
│ │ ├── BookingView.vue # 预约首页(选择寄养/洗护)
│ │ ├── BoardingBookingView.vue # 寄养预约(4 步 Stepper:套餐→房型日期→宠物→提交)
│ │ └── GroomingBookingView.vue # 洗护预约(4 步 Stepper:套餐→时段→宠物→提交)
│ │
│ ├── order/ # 订单管理模块
│ │ ├── OrderListView.vue # 我的订单列表(Tab 筛选,状态标签)
│ │ └── OrderDetailView.vue # 订单详情(信息展示,支付/取消按钮)
│ │
│ ├── pet/ # 宠物档案模块
│ │ ├── PetListView.vue # 宠物列表(卡片展示,增删查改)
│ │ ├── PetCreateView.vue # 新增宠物表单
│ │ ├── PetDetailView.vue # 宠物详情(基本信息+疫苗记录+驱虫记录)
│ │ └── PetEditView.vue # 编辑宠物表单
│ │
│ ├── profile/ # 个人中心模块
│ │ ├── ProfileView.vue # 个人资料展示
│ │ ├── ProfileEditView.vue # 修改个人资料
│ │ └── PasswordEditView.vue # 修改密码
│ │
│ └── admin/ # 商家后台模块
│ ├── AdminOrderListView.vue # 订单审核列表(表格+筛选+审核/取消处理弹窗)
│ ├── AdminOrderDetailView.vue # 订单审核详情
│ ├── AdminRoomCalendarView.vue # 宠物房间管理(房型选择+月份选择+日历)
│ ├── AdminScheduleListView.vue # 排班管理(表格+新增弹窗+删除)
│ ├── AdminPackageListView.vue # 套餐配置列表
│ ├── AdminPackageCreateView.vue # 新增套餐表单
│ ├── AdminPackageEditView.vue # 编辑套餐表单
│ └── AdminUserDetailView.vue # 用户详情(含宠物档案列表)
│
└── components/ # 公共可复用组件
├── RoomCalendar.vue # 宠物房间日历(props: roomId/month/statusList, events: date-click)
├── PaymentDialog.vue # 模拟支付弹窗(props: visible/amount/orderId, events: success/close)
└── PetSelector.vue # 宠物选择器(props: petList/selected, events: update:selected)
flowchart TD
A["/login<br/>(LoginView)"] --- B[""/""]
B --> C["UserLayout<br/>用户侧布局"]
C --> C1["/booking<br/>服务预约"]
C --> C2["/booking/boarding<br/>寄养预约*"]
C --> C3["/booking/grooming<br/>洗护预约*"]
C --> C4["/orders<br/>我的订单*"]
C --> C5["/orders/:id<br/>订单详情*"]
C --> C6["/pets<br/>宠物档案*"]
C --> C7["/pets/new<br/>新增宠物*"]
C --> C8["/pets/:id<br/>宠物详情*"]
C --> C9["/pets/:id/edit<br/>编辑宠物*"]
C --> C10["/profile<br/>个人中心*"]
C --> C11["/profile/edit<br/>修改资料*"]
C --> C12["/profile/password<br/>修改密码*"]
B --> D["AdminLayout<br/>商家后台布局*"]
D --> D1["/admin/orders<br/>订单审核"]
D --> D2["/admin/orders/:id<br/>审核详情"]
D --> D3["/admin/rooms<br/>宠物房间管理"]
D --> D4["/admin/schedules<br/>排班管理"]
D --> D5["/admin/packages<br/>套餐配置"]
D --> D6["/admin/packages/new<br/>新增套餐"]
D --> D7["/admin/packages/:id/edit<br/>编辑套餐"]
D --> D8["/admin/users<br/>用户管理"]
D --> D9["/admin/users/:id<br/>用户详情"]
style D fill:#fce7f3,stroke:#db2777
style C fill:#e8f5e9,stroke:#2e7d32
style A fill:#fff3e0,stroke:#f57c00
*标记的路由需要requiresAuth: true(登录校验)
/admin前缀下的所有路由需要requiresAdmin: true(管理员角色校验)
flowchart TD
A["Router Navigation"] --> B{"to.matched<br/>有 requiresAuth<br/>或 requiresAdmin?"}
B -- 否 --> C["next()<br/>放行"]
B -- 是 --> D{"loginUserStore<br/>.isLogin?"}
D -- 否 --> E["ElMessage.warning('请先登录')"]
E --> F["next({ path: '/login',<br/> query: { redirect } })"]
D -- 是 --> G{"requiresAdmin &&<br/>role !== 'admin'?"}
G -- 是 --> H["ElMessage.error('无权访问')"]
H --> I["next('/booking')"]
G -- 否 --> C
守卫逻辑摘要:
requiresAuth 标记在需要登录的路由 meta 上(预约、订单、宠物、个人中心等)requiresAdmin 标记在 /admin 布局路由的 meta 上,子路由自动继承redirect 参数在登录后导航回原始目标页面| 路径 | 名称 | 组件 | meta.title | meta.icon | requiresAuth |
|---|---|---|---|---|---|
/login |
login |
LoginView | - | - | ❌ |
/ |
- | UserLayout (布局) | - | - | ❌ |
/booking |
booking |
BookingView | 服务预约 | Calendar | ❌ |
/booking/boarding |
boardingBooking |
BoardingBookingView | 寄养预约 | - | ✅ |
/booking/grooming |
groomingBooking |
GroomingBookingView | 洗护预约 | - | ✅ |
/orders |
orderList |
OrderListView | 我的订单 | Tickets | ✅ |
/orders/:id |
orderDetail |
OrderDetailView | 订单详情 | - | ✅ |
/pets |
petList |
PetListView | 宠物档案 | Avatar | ✅ |
/pets/new |
petCreate |
PetCreateView | 新增宠物 | - | ✅ |
/pets/:id |
petDetail |
PetDetailView | 宠物详情 | - | ✅ |
/pets/:id/edit |
petEdit |
PetEditView | 编辑宠物 | - | ✅ |
/profile |
profile |
ProfileView | 个人中心 | User | ✅ |
/profile/edit |
profileEdit |
ProfileEditView | 修改资料 | - | ✅ |
/profile/password |
passwordEdit |
PasswordEditView | 修改密码 | - | ✅ |
| 路径 | 名称 | 组件 | meta.title | meta.icon | requiresAdmin |
|---|---|---|---|---|---|
/admin |
adminLayout |
AdminLayout (布局) | - | - | ✅ |
/admin/orders |
adminOrderList |
AdminOrderListView | 订单审核 | List | ✅ |
/admin/orders/:id |
adminOrderDetail |
AdminOrderDetailView | 订单审核详情 | - | ✅ |
/admin/rooms |
adminRooms |
AdminRoomCalendarView | 房态管理 | Calendar | ✅ |
/admin/schedules |
adminSchedules |
AdminScheduleListView | 排班管理 | EditPen | ✅ |
/admin/packages |
adminPackageList |
AdminPackageListView | 套餐配置 | Coin | ✅ |
/admin/packages/new |
adminPackageCreate |
AdminPackageCreateView | 新增套餐 | - | ✅ |
/admin/packages/:id/edit |
adminPackageEdit |
AdminPackageEditView | 编辑套餐 | - | ✅ |
/admin/users |
adminUsers |
UserView | 用户管理 | User | ✅ |
/admin/users/:id |
adminUserDetail |
AdminUserDetailView | 用户详情 | - | ✅ |
┌──────────────────────────────────────────────────────────────────┐
│ [Logo] 宠物服务管理系统 [服务预约] [我的订单] [宠物档案] [个人中心] │ ← el-header
│ [后台管理] [用户名 ▼] [登录/注册] │
├──────────────────────────────────────────────────────────────────┤
│ │
│ <router-view /> │ ← el-main
│ │
└──────────────────────────────────────────────────────────────────┘
导航菜单渲染:
userRoutes[0].children 中筛选出有 meta.icon 的菜单项(过滤掉无图标的详情页)route.path 高亮/admin用户状态区域:
退出逻辑:
const logoutHandler = async () => {
await getAuthController().logout() // 服务端销毁 Session
loginUserStore.logoutUser() // 清除 Pinia 状态
router.push('/login')
}
┌──────────────────────────────────────────────────────────────────┐
│ [Logo] 宠物服务管理系统 后台管理 [用户名 ▼] │ ← el-header
├────────────┬─────────────────────────────────────────────────────┤
│ [展开/收起]│ │
│ │ <router-view /> │ ← el-main
│ 订单审核 │ │
│ 房态管理 │ │
│ 排班管理 │ │
│ 套餐配置 │ │
│ 用户管理 │ │
└────────────┴─────────────────────────────────────────────────────┘
可折叠侧边栏:
el-aside 宽度默认 200px,收起时 64pxel-radio-group 展开/收起切换(isCollapse ref)adminRoutes[0].children 中筛选有 meta.icon 的项顶部栏右侧用户下拉:
用途: 在寄养预约和宠物房间管理页面中展示指定房型某月的每日状态。
┌──────────────────────────────┐
│ < 2026年7月 > │
├──────────────────────────────┤
│ 日 一 二 三 四 五 六 │
│ 1 2 3 4 │
│ 5 6 7 8 9 10 11 │
│ ... │
└──────────────────────────────┘
绿色=AVAILABLE 红色=OCCUPIED
Props:
| prop | 类型 | 必填 | 说明 |
|---|---|---|---|
roomId |
string |
✅ | 房型 ID,用于标识当前展示的房型 |
month |
string |
✅ | 格式 YYYY-MM,如 2026-07 |
statusList |
{ date: string; status: 'AVAILABLE' \| 'OCCUPIED' }[] |
✅ | 当月每日状态列表 |
Emits:
| event | 参数 | 说明 |
|---|---|---|
date-click |
(date: string, status: 'AVAILABLE' \| 'OCCUPIED') |
点击某天时触发 |
month-change |
(month: string) |
月份切换时触发(TODO: 当前组件中 prevMonth/nextMonth 为占位实现) |
渲染逻辑:
month 计算当月天数、第一天是星期几(DayInfo | null)[][])null 填充statusMap 查找状态,应用 CSS 类:available(绿色 #e8f5e9)、occupied(红色 #ffebee)用途: 预约提交成功后弹出,模拟支付定金流程。
Props:
| prop | 类型 | 必填 | 说明 |
|---|---|---|---|
visible |
boolean |
✅ | 弹窗显隐(v-model:visible) |
amount |
number |
✅ | 定金金额 |
orderId |
string |
✅ | 订单 ID,用于调用支付接口 |
Emits:
| event | 参数 | 说明 |
|---|---|---|
update:visible |
(value: boolean) |
弹窗显隐变化 |
success |
无 | 支付成功后触发 |
close |
无 | 弹窗关闭时触发 |
插槽: 无
交互流程:
¥{{ amount }}el-radio-group)POST /api/appointments/{orderId}/pay-depositElMessage.success('支付成功') → emit('success') → 关闭弹窗ElMessage.error 显示错误信息用途: 在预约流程中选择宠物。
Props:
| prop | 类型 | 必填 | 说明 |
|---|---|---|---|
petList |
Pet[] |
✅ | 用户的所有宠物列表 |
selected |
Pet \| null |
✅ | 当前选中的宠物 |
Emits:
| event | 参数 | 说明 |
|---|---|---|
update:selected |
(pet: Pet) |
选择宠物时触发 |
插槽: 无
渲染逻辑:
el-empty +「新增宠物」按钮(跳转 /pets/new)el-card 列表,每张卡片显示宠物首字母头像、名称、品种、年龄border-color: var(--el-color-primary))+ 勾选图标布局:
┌──────────────────────────────────────┐
│ ← 返回主页 │
│ │
│ 欢 迎 │
│ 请登录或注册您的账号 │
│ │
│ ┌────────────┬────────────┐ │
│ │ 登录 │ 注册 │ │ ← el-tabs
│ └────────────┴────────────┘ │
│ │
│ ┌────────────────────────┐ │
│ │ 👤 请输入账号 │ │
│ ├────────────────────────┤ │
│ │ 🔒 请输入密码 │ │
│ ├────────────────────────┤ │
│ │ [ 登录 ] │ │ ← 渐变粉色按钮
│ └────────────────────────┘ │
└──────────────────────────────────────┘
数据流:
sequenceDiagram
actor U as User
participant V as LoginView
participant S as useLoginUserStore
participant A as auth-controller
participant B as Backend
U->>V: 填写账号密码
U->>V: 点击登录
V->>V: formRef.validate()
alt 校验失败
V->>U: 显示错误提示
else 校验通过
V->>A: authApi.login({ account, password })
A->>B: POST /api/auth/login
B->>B: 校验账号密码
B->>B: 存入 HttpSession
B-->>A: Response<UserVo>
A-->>V: res.data
V->>S: loginSuccess({ id, name, role })
V->>V: router.push(redirect || '/booking')
V->>U: ElMessage.success('登录成功')
end
表单校验规则:
| 表单 | 字段 | 规则 |
|---|---|---|
| 登录 | account | required |
| 登录 | password | required, min:4 |
| 注册 | account | required, min:4, max:20 |
| 注册 | username | required, min:2, max:32 |
| 注册 | password | required, min:4, max:20 |
| 注册 | confirmPassword | required, 自定义校验器:必须等于 password |
API 调用:
getAuthController().login(LoginDto) → POST /api/auth/login → 服务端存入 Session → 返回 UserVogetAuthController().register(RegisterDto) → POST /api/auth/register → 返回成功 → 切换到登录 tab状态管理:
activeTab: ref('login') — 当前 tabloginForm: reactive({ account, password })registerForm: reactive({ account, username, password, confirmPassword })loginLoading, registerLoading: ref(false)布局:
┌──────────────────────────────────────┐
│ │
│ 宠物服务管理系统 │
│ 专业宠物寄养与洗护服务平台 │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ 🏠 寄养服务 │ │ ✂️ 洗护服务 │ │
│ │ 专业宠物寄养 │ │ 专业宠物洗护 │ │
│ │ 安心托管 │ │ 焕然一新 │ │
│ │ [查看详情] │ │ [查看详情] │ │
│ └──────────────┘ └──────────────┘ │
│ │
│ 功能介绍 / 平台优势展示 │
│ │
└──────────────────────────────────────┘
交互逻辑:
router.push('/booking/boarding')router.push('/booking/grooming')requiresAuth 拦截)数据依赖: 无(纯展示页面)
┌──────────────────────────────────────┐
│ 选择服务类型 │
│ │
│ ┌────────────────┐ ┌────────────────┐│
│ │ 寄养服务 │ │ 洗护服务 ││
│ │ 专业宠物寄养 │ │ 专业宠物洗护 ││
│ │ 安心托管 │ │ 焕然一新 ││
│ │ │ │ ││
│ │ [查看详情] │ │ [查看详情] ││
│ └────────────────┘ └────────────────┘│
└──────────────────────────────────────┘
数据流: 无 API 调用,纯导航跳转
多步表单流程(el-steps 4 步):
sequenceDiagram
actor U as User
participant V as BoardingBookingView
participant A as API Layer
participant B as Backend
Note over U,V: Step 1 — 选择套餐
V->>A: getPackageController().list4({ type: 'BOARDING' })
A->>B: GET /api/packages?type=BOARDING
B-->>A: PackageVo[]
A-->>V: packages[]
U->>V: 选中一个套餐
Note over U,V: Step 2 — 选择房型与日期
V->>A: getBoardingRoomController().listRooms()
A->>B: GET /api/boarding-rooms
B-->>A: BoardingRoomVo[]
A-->>V: rooms[]
U->>V: 选中房型
V->>A: getBoardingRoomController().getStatus(roomId, { month })
A->>B: GET /api/boarding-rooms/{id}/status?month=2026-07
B-->>A: RoomStatusVo[]
A-->>V: roomStatusList[]
V->>V: RoomCalendar 渲染日历
U->>V: 选择入住/离店日期
V->>V: calcTotal() — 计算总价(天数 × pricePerNight)
Note over U,V: Step 3 — 选择宠物
V->>A: getPetController().list()
A->>B: GET /api/pets
B-->>A: PetVo[]
A-->>V: myPets[]
U->>V: PetSelector 选中宠物
Note over U,V: Step 4 — 确认提交
V->>V: 展示订单摘要
U->>V: 点击提交预约
V->>A: getAppointmentController().create(AppointmentDto)
A->>B: POST /api/appointments { packageId, roomId, petId, startDate, endDate }
B-->>A: AppointmentVo { id, deposit, ... }
A-->>V: createdOrderId, depositAmount
V->>V: PaymentDialog 弹窗
U->>V: 支付定金
V->>A: getAppointmentController().payDeposit(orderId)
A->>B: POST /api/appointments/{id}/pay-deposit
B-->>A: success
V->>V: router.push('/orders')
组件树:
flowchart TD
V["BoardingBookingView"]
V --> S["el-steps (4步)"]
V --> Step1["Step1: 套餐选择"]
Step1 --> PkgList["el-radio-group > el-card<br/>(API: GET packages)"]
V --> Step2["Step2: 房型与日期"]
Step2 --> RoomSel["el-select 房型列表<br/>(API: GET rooms)"]
Step2 --> DatePicker["el-date-picker daterange"]
Step2 --> Calendar["RoomCalendar 宠物房间日历<br/>(API: GET room status)"]
Step2 --> Price["总价显示 ¥xxx"]
V --> Step3["Step3: 宠物选择"]
Step3 --> PetSel["PetSelector<br/>(API: GET pets)"]
V --> Step4["Step4: 确认提交"]
Step4 --> Summary["el-descriptions 订单摘要"]
Step4 --> Btn["提交预约"]
V --> Payment["PaymentDialog<br/>支付弹窗 (API: payDeposit)"]
价格计算逻辑:
const calcTotal = () => {
if (dateRange.value && selectedRoom.value) {
const days = Math.ceil(
(dateRange.value[1].getTime() - dateRange.value[0].getTime()) / 86400000
)
totalPrice.value = days * selectedRoom.value.pricePerNight
}
}
// 定金 = totalPrice × 30%
API 调用列表:
GET /api/packages?type=BOARDING — 获取寄养套餐GET /api/boarding-rooms — 获取所有启用房型GET /api/boarding-rooms/{id}/status?month=YYYY-MM — 获取房态GET /api/pets — 获取用户宠物POST /api/appointments — 提交预约POST /api/appointments/{id}/pay-deposit — 支付定金sequenceDiagram
actor U as User
participant V as GroomingBookingView
participant A as API Layer
Note over U,V: Step 1 — 选择套餐
V->>A: getPackageController().list4({ type: 'GROOMING' })
A-->>V: packages[]
U->>V: 选中套餐
Note over U,V: Step 2 — 选择时间
U->>V: 选择日期 + 时段(上午/下午)
Note over U,V: Step 3 — 选择宠物
V->>A: getPetController().list()
A-->>V: myPets[]
U->>V: PetSelector 选中宠物
Note over U,V: Step 4 — 确认提交
V->>A: getAppointmentController().create(AppointmentDto)
A-->>V: createdOrderId
V->>V: PaymentDialog 弹窗
U->>V: 支付定金
组件树:
flowchart TD
V["GroomingBookingView"]
V --> S["el-steps (4步)"]
V --> Step1["Step1: 套餐选择"]
Step1 --> PkgList["el-radio-group > el-card<br/>(API: GET packages?type=GROOMING)"]
V --> Step2["Step2: 时段选择"]
Step2 --> Date["el-date-picker"]
Step2 --> Slot["el-radio-group MORNING/AFTERNOON"]
V --> Step3["Step3: 宠物选择"]
Step3 --> PetSel["PetSelector<br/>(API: GET pets)"]
V --> Step4["Step4: 确认提交"]
Step4 --> Summary["el-descriptions 订单摘要"]
Step4 --> Btn["提交预约"]
V --> Payment["PaymentDialog<br/>(API: payDeposit)"]
API 调用列表:
GET /api/packages?type=GROOMING — 获取洗护套餐GET /api/pets — 获取宠物列表POST /api/appointments — 提交预约(type=GROOMING)POST /api/appointments/{id}/pay-deposit — 支付定金布局:
┌──────────────────────────────────────┐
│ 我的预约 │
│ [全部] [待确认] [已确认] [已完成] [已取消]│ ← el-tabs
├──────────────────────────────────────┤
│ ┌──────────────────────────────────┐│
│ │ 订单 ID: 1 [PENDING] 待审核 ││
│ │ [查看详情] ││
│ └──────────────────────────────────┘│
│ ┌──────────────────────────────────┐│
│ │ 订单 ID: 2 [CONFIRMED] 已确认││
│ │ [查看详情] ││
│ └──────────────────────────────────┘│
└──────────────────────────────────────┘
组件树:
flowchart TD
V["OrderListView"]
V --> Tabs["el-tabs 状态筛选"]
V --> Cards["el-card 列表"]
Cards --> Card["el-card × N"]
Card --> Tag["el-tag 状态标签"]
Card --> Btn["查看详情 → router.push /orders/:id"]
状态标签颜色映射:
| 状态 | el-tag type | 中文 |
|---|---|---|
| PENDING | warning | 待审核 |
| CONFIRMED | success | 已确认 |
| CANCELLED | info | 已取消 |
| COMPLETED | primary | 已完成 |
| CANCEL_REQUESTED | danger | 取消申请 |
API 调用:
GET /api/appointments → 返回所有用户预约列表 → 前端按 activeStatus 客户端过滤布局:
┌──────────────────────────────────────┐
│ ← 返回 │
├──────────────────────────────────────┤
│ 订单详情 │
│ ┌──────────────────────────────────┐│
│ │ 订单编号: 1 状态: [PENDING] ││
│ │ 服务类型: BOARDING ││
│ │ 总价: ¥450 ││
│ │ 定金: ¥135 定金支付: 未支付 ││
│ │ 创建时间: 2026-07-01 10:00 ││
│ │ ││
│ │ [支付定金] [申请取消] ││
│ └──────────────────────────────────┘│
└──────────────────────────────────────┘
条件按钮逻辑:
showPayDeposit: depositPaid === false && status === 'PENDING'showCancel: status === 'PENDING' || status === 'CONFIRMED'API 调用:
GET /api/appointments/{id} — 获取订单详情POST /api/appointments/{id}/cancel — 申请取消(ElMessageBox.confirm + 输入原因)POST /api/appointments/{id}/pay-deposit — 支付定金┌──────────────────────────────────────┐
│ 宠物档案 [+ 新增宠物] │
├──────────────────────────────────────┤
│ ┌──────────────────────────────────┐│
│ │ [W] 旺财 - 金毛 ││
│ │ 2岁 | 公 ││
│ │ [查看详情] [编辑] [删除] ││
│ └──────────────────────────────────┘│
│ ┌──────────────────────────────────┐│
│ │ ... ││
│ └──────────────────────────────────┘│
└──────────────────────────────────────┘
API 调用:
GET /api/pets — 获取所有宠物DELETE /api/pets/{id} — 删除宠物(ElMessageBox.confirm 确认)┌──────────────────────────────────────┐
│ ← 返回 [编辑] [删除]│
├──────────────────────────────────────┤
│ 基本信息 │
│ 名称: 旺财 | 品种: 金毛 │
│ 年龄: 2岁 | 体重: 25kg │
│ 性别: 公 | 备注: 活泼好动 │
├──────────────────────────────────────┤
│ 疫苗记录 [+ 新增] │
│ ┌──────────────────────────────────┐│
│ │ 狂犬疫苗 | 2026-01-01 | 安心医院 ││
│ └──────────────────────────────────┘│
├──────────────────────────────────────┤
│ 驱虫记录 [+ 新增] │
│ ┌──────────────────────────────────┐│
│ │ 体外驱虫 | 2026-03-15 | 福来恩 ││
│ └──────────────────────────────────┘│
└──────────────────────────────────────┘
API 调用:
GET /api/pets/{id} — 获取宠物详情(含疫苗/驱虫记录)DELETE /api/pets/{id} — 删除宠物POST /api/pets/{id}/vaccinations — 新增疫苗记录POST /api/pets/{id}/deworming — 新增驱虫记录新增记录弹窗表单字段:
┌──────────────────────────────────────┐
│ ← 返回 │
│ 新增宠物 / 编辑宠物 │
│ │
│ 宠物名: [__________] │
│ 品种: [__________] │
│ 年龄: [0] │
│ 体重: [0.0] │
│ 性别: ○ 公 ○ 母 │
│ 备注: [__________] │
│ │
│ [保存] [取消] │
└──────────────────────────────────────┘
API 调用(PetCreateView):
POST /api/pets — 新增宠物API 调用(PetEditView):
GET /api/pets/{id} — 加载宠物数据填充表单PUT /api/pets/{id} — 更新宠物信息ProfileView 布局:
┌──────────────────────────────────────┐
│ 个人中心 │
│ ┌──────────────────────────────────┐│
│ │ [大头像] ││
│ │ 用户名 ││
│ │ [admin/user] ││
│ ├──────────────────────────────────┤│
│ │ ID: 1 ││
│ │ 昵称: 张三 [编辑] ││
│ ├──────────────────────────────────┤│
│ │ [修改密码] ││
│ └──────────────────────────────────┘│
└──────────────────────────────────────┘
ProfileEditView:
PUT /api/user — 更新用户信息 → 更新 Pinia storePasswordEditView:
PUT /api/user/updatePassword — 修改密码sequenceDiagram
actor A as Admin
participant V as AdminOrderListView
participant API as API Layer
participant B as Backend
Note over V: 页面加载
V->>API: getAdminAppointmentController().listAll({ pageNum, pageSize, status? })
API->>B: GET /api/admin/appointments?pageNum=1&pageSize=10
B-->>API: PageVo<AppointmentVo>
API-->>V: orders[]
Note over A,V: 审核操作
A->>V: 点击"审核"
V->>V: 打开审核弹窗
A->>V: 选择通过/拒绝 + 填写原因
V->>API: review(appointmentId, { action, rejectReason? })
API->>B: PUT /api/admin/appointments/{id}/review
B-->>API: success
API-->>V: ElMessage.success
V->>V: 刷新列表
Note over A,V: 处理取消申请
A->>V: 点击"处理取消"
V->>V: 打开取消处理弹窗
A->>V: 选择同意/拒绝
V->>API: handleCancel(appointmentId, { action })
API->>B: PUT /api/admin/appointments/{id}/handle-cancel
B-->>API: success
V->>V: 刷新列表
AdminOrderListView 组件树:
flowchart TD
V["AdminOrderListView"]
V --> Filter["el-select 状态筛选"]
V --> Table["el-table 订单列表"]
Table --> Cols["ID | 用户 | 类型 | 总价 | 状态标签 | 操作"]
Table --> Pagination["el-pagination"]
V --> ReviewDlg["审核弹窗"]
ReviewDlg --> Radio["通过/拒绝 radio"]
ReviewDlg --> Reason["原因 textarea"]
V --> CancelDlg["取消处理弹窗"]
CancelDlg --> CancelRadio["同意/拒绝 radio"]
CancelDlg --> Opinion["处理意见 textarea"]
AdminOrderDetailView: 与 AdminOrderListView 共享审核/取消弹窗逻辑,以详情页形式展示完整订单信息。
flowchart TD
A["商家进入宠物房间管理"]
A --> B["选择房型<br/>el-select 切换"]
A --> C["选择月份<br/>el-date-picker type=month"]
B --> D["RoomCalendar 宠物房间日历"]
C --> D
D --> E["点击某天"]
E --> F{"当前状态?"}
F -->|"AVAILABLE"| G["切换为 OCCUPIED<br/>PUT /api/admin/rooms/{id}/status"]
F -->|"OCCUPIED"| H["切换为 AVAILABLE<br/>PUT /api/admin/rooms/{id}/status"]
G --> I["刷新日历 + ElMessage.success"]
H --> I
布局:
┌──────────────────────────────────────────────┐
│ 宠物房间管理 │
│ [房型选择 ▼] [2026年07月 ▼] │
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐│
│ │ 宠物房间日历 ││
│ │ 日 一 二 三 四 五 六 ││
│ │ 1 2 3 4 ││
│ │ 5 6 7 8 9 10 11 ││
│ │ 12 13 14 15 16 17 18 ││
│ │ ... ││
│ │ 绿色=空闲 红色=占用 ││
│ └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘
CRITICAL: 使用"宠物房间管理" / "宠物房间日历" 术语:
API 调用:
GET /api/boarding-rooms — 获取所有房型GET /api/boarding-rooms/{id}/status?month=YYYY-MM — 获取房态PUT /api/admin/boarding-rooms/{roomId}/status — 修改单日状态(切换 AVAILABLE/OCCUPIED)PUT /api/admin/boarding-rooms/{roomId}/status/batch — 批量修改(预留,当前 UI 暂未实现多选)状态切换逻辑(handleDateClick):
const handleDateClick = async (date: string, status: 'AVAILABLE' | 'OCCUPIED') => {
const newStatus = status === 'AVAILABLE' ? 'OCCUPIED' : 'AVAILABLE'
// PUT /api/admin/boarding-rooms/{roomId}/status { date, status: newStatus }
// 刷新日历
}
┌──────────────────────────────────────────────┐
│ 排班管理 [+ 新增] │
│ [2026年07月 ▼] │
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐│
│ │ 设计师 | 日期 | 时段 | 状态 | 操作 ││
│ │ 张三 | 07-01 | 上午 | 可用 | [删除] ││
│ │ 李四 | 07-01 | 下午 | 可用 | [删除] ││
│ │ ... ││
│ └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘
新增弹窗:
┌──────────────────────────┐
│ 新增排班 │
│ 设计师: [__________] │
│ 日期: [date ▼] │
│ 时段: [上午 ▼] │
│ 是否可用: [🟢] │
│ │
│ [取消] [保存] │
└──────────────────────────┘
API 调用:
GET /api/admin/schedules?month=YYYY-MM — 获取排班列表POST /api/admin/schedules — 新增排班DELETE /api/admin/schedules/{id} — 删除排班┌──────────────────────────────────────────────┐
│ 套餐配置 [+ 新增套餐] │
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐│
│ │ 名称 | 类型 | 价格 | 天数 | 状态 | 操作 ││
│ │ 标准寄养 | 寄养 | ¥100 | 7天 | 启用 | [编辑][删]││
│ │ 基础洗护 | 洗护 | ¥80 | - | 启用 | [编辑][删]││
│ └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘
新增/编辑表单:
┌──────────────────────────────────┐
│ 新增套餐 / 编辑套餐 │
│ 套餐类型: ○ 寄养 ○ 洗护 │
│ 套餐名称: [______________] │
│ 价格: [0.00] │
│ 描述: [______________] │
│ 寄养天数: [1] (仅寄养显示) │
│ 是否启用: [🟢] │
│ │
│ [取消] [保存] │
└──────────────────────────────────┘
API 调用:
GET /api/packages — 获取套餐列表GET /api/packages/{id} — 获取单个套餐POST /api/admin/packages — 新增套餐PUT /api/admin/packages/{id} — 更新套餐DELETE /api/admin/packages/{id} — 删除套餐(逻辑删除)AdminUserListView 复用 UserView.vue:
AdminUserDetailView:
┌──────────────────────────────────────────────┐
│ ← 用户管理 │
├──────────────────────────────────────────────┤
│ 用户信息 │
│ ┌──────────────────────────────────────────┐│
│ │ 用户名: 张三 | 账号: zhangsan ││
│ │ 角色: user | 邮箱: z@x.com ││
│ │ 手机: 138xxxx | ││
│ └──────────────────────────────────────────┘│
├──────────────────────────────────────────────┤
│ 该用户的宠物档案 │
│ ┌──────────────────────────────────────────┐│
│ │ 名称 | 品种 | 性别 | 体重 ││
│ │ 旺财 | 金毛 | 公 | 25kg ││
│ └──────────────────────────────────────────┘│
└──────────────────────────────────────────────┘
API 调用:
GET /api/admin/users/{id} — 获取用户详情(含用户信息 + 宠物列表)GET /api/user 分页查询store/user.ts)| 分类 | 名称 | 类型 | 说明 |
|---|---|---|---|
| State | loginUser |
Reactive<LoginUser> |
{ isLogin: false, user: { id, name, role } } |
| Getter | userId |
ComputedRef<string> |
loginUser.user?.id |
| Action | updateUser(user: User) |
void |
替换 user 对象 |
| Action | loginSuccess(user: User) |
void |
设置 isLogin=true + 更新 user |
| Action | logoutUser() |
void |
重置为未登录状态 |
持久化: persist: true — 通过 pinia-plugin-persistedstate 自动同步到 localStorage
store/meta.ts)| 分类 | 名称 | 类型 | 说明 |
|---|---|---|---|
| State | title |
Ref<string> |
网站标题(默认"标题") |
| State | logo |
Ref<string> |
Logo 路径(默认 /logo.svg) |
| State | baseUrl |
Ref<string> |
后端地址(默认 http://localhost:8080/) |
持久化: persist: true
store/pet.ts)| 分类 | 名称 | 类型 | 说明 |
|---|---|---|---|
| State | pets |
Ref<Pet[]> |
宠物列表缓存 |
| Action | setPets(list: Pet[]) |
void |
覆盖设置宠物列表 |
| Action | addPet(pet: Pet) |
void |
追加一只宠物 |
| Action | updatePet(updated: Pet) |
void |
按 id 更新宠物 |
| Action | removePet(id: string) |
void |
按 id 删除宠物 |
store/appointment.ts)| 分类 | 名称 | 类型 | 说明 |
|---|---|---|---|
| State | currentPackage |
Ref<Package \| null> |
当前选中套餐 |
| State | selectedPet |
Ref<Pet \| null> |
当前选中宠物 |
| State | selectedRoom |
Ref<BoardingRoom \| null> |
当前选中房型 |
| State | dateRange |
Ref<[Date, Date] \| null> |
入住/离店日期范围 |
| Action | reset() |
void |
重置所有状态为 null |
注意:目前 BoardingBookingView 使用局部 ref 而非此 store 管理状态。
// util/axios-instance.ts
const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // Vite 代理前缀
timeout: 30000,
withCredentials: true, // 自动携带 JSESSIONID Cookie
headers: { 'Content-Type': 'application/json' }
})
// 响应拦截器
axiosInstance.interceptors.response.use(
(response) => {
if (response.data.code !== 0 && response.data.code !== 200) {
return Promise.reject(new Error(response.data.message || '请求失败'))
}
return response.data // 解包,直接返回 { code, message, data }
},
(error) => {
if (error.response?.status === 401) {
window.location.href = '/login' // Session 过期 → 跳转登录
}
return Promise.reject(error)
}
)
// Orval 自定义实例
export const customAxiosInstance = <T>(config: CustomInstanceConfig): Promise<T> => {
return axiosInstance(config)
}
| 文件 | 工厂函数 | 主要方法 | 对应 API 端点 |
|---|---|---|---|
auth-controller.ts |
getAuthController() |
login, register, logout |
POST /api/auth/login, POST /api/auth/register, ... |
user-controller.ts |
getUserController() |
queryByPageUsers, add, edit, deleteById, updateStatus, updatePassword, updateAvatar, queryById |
GET/POST/PUT/DELETE /api/user/** |
pet-controller.ts |
getPetController() |
list, add1, getById, update, _delete, addVaccination, addDeworming |
GET/POST/PUT/DELETE /api/pets/** |
appointment-controller.ts |
getAppointmentController() |
create, list1, getById2, cancel, payDeposit |
POST/GET /api/appointments/** |
admin-appointment-controller.ts |
getAdminAppointmentController() |
listAll, review, handleCancel |
GET/PUT /api/admin/appointments/** |
boarding-room-controller.ts |
getBoardingRoomController() |
listRooms, getStatus |
GET /api/boarding-rooms/** |
admin-boarding-room-controller.ts |
getAdminBoardingRoomController() |
updateStatus1, batchUpdateStatus |
PUT /api/admin/boarding-rooms/** |
package-controller.ts |
getPackageController() |
list4, getById1 |
GET /api/packages/** |
admin-package-controller.ts |
getAdminPackageController() |
create2, update1, delete1 |
POST/PUT/DELETE /api/admin/packages/** |
admin-schedule-controller.ts |
getAdminScheduleController() |
list2, create1, delete2 |
GET/POST/DELETE /api/admin/schedules/** |
admin-user-controller.ts |
getAdminUserController() |
listAllUsers, getDetail |
GET /api/admin/users/** |
schedule-controller.ts |
getScheduleController() |
(预留) | - |
sequenceDiagram
actor U as User
participant V as LoginView
participant S as useLoginUserStore
participant A as Axios Instance
participant B as Backend
U->>V: 输入账号密码
U->>V: 点击登录
V->>V: formRef.validate()
alt 校验失败
V->>U: 显示表单错误
else 校验通过
V->>A: authApi.login({ account, password })
A->>B: POST /api/auth/login<br/>withCredentials: true
B->>B: MD5 校验密码
B->>B: request.getSession().setAttribute("user", userInfo)
B-->>A: Set-Cookie: JSESSIONID=xxx<br/>Response<UserVo>
A-->>V: res.data (UserVo)
V->>S: loginSuccess({ id, name, role })
S->>S: loginUser.isLogin = true
S->>S: persist → localStorage
V->>V: router.push(redirect || '/booking')
V->>U: ElMessage.success('登录成功')
end
Note over U,B: 后续请求
U->>V: 访问需登录页面
V->>A: 任意 API 请求
A->>B: Cookie: JSESSIONID=xxx
B->>B: session.getAttribute("user")
B-->>A: 正常响应
sequenceDiagram
actor U as User
participant V as BoardingBookingView
participant S as useAppointmentStore
participant A as API Layer
participant B as Backend
Note over V: Step 1 — 选择套餐
V->>A: GET /api/packages?type=BOARDING
A->>B: 查询套餐表
B-->>A: PackageVo[] (is_active=true)
A-->>V: 卡片列表渲染
U->>V: 点击选择套餐
V->>S: currentPackage = selected
Note over V: Step 2 — 选择房型与日期
V->>A: GET /api/boarding-rooms
A-->>V: BoardingRoomVo[]
U->>V: 选择房型
V->>A: GET /api/boarding-rooms/{id}/status?month=2026-07
A-->>V: RoomStatusVo[]
V->>V: 渲染 RoomCalendar 宠物房间日历
U->>V: 选择入住/离店日期
V->>V: 计算天数、总价
V->>S: selectedRoom, dateRange
Note over V: Step 3 — 选择宠物
V->>A: GET /api/pets
A-->>V: PetVo[]
V->>V: 渲染 PetSelector
U->>V: 选择宠物
V->>S: selectedPet
Note over V: Step 4 — 提交 + 支付
U->>V: 点击提交预约
V->>A: POST /api/appointments<br/>{ packageId, roomId, petId, startDate, endDate }
A->>B: 创建预约记录 (status=PENDING)
B-->>A: AppointmentVo { id, deposit, totalPrice }
A-->>V: createdOrderId, depositAmount
V->>V: 打开 PaymentDialog
U->>V: 选择支付方式
U->>V: 点击确认支付
V->>A: POST /api/appointments/{id}/pay-deposit
A->>B: 更新 depositPaid=true
B-->>A: success
V->>U: ElMessage.success('支付成功')
V->>V: router.push('/orders')
sequenceDiagram
actor U as User
participant V as OrderDetailView
participant A as API Layer
participant B as Backend
alt PENDING 状态取消
U->>V: 点击"申请取消"
V->>V: ElMessageBox.confirm
U->>V: 确认 + 填写原因
V->>A: POST /api/appointments/{id}/cancel<br/>{ cancelReason }
A->>B: 校验状态为 PENDING
B->>B: status → CANCELLED
alt 已支付定金
B->>B: depositRefunded = true
end
B->>B: 释放房间状态 (若有)
B-->>A: success
A-->>V: ElMessage.success('取消成功')
V->>V: 刷新订单详情
else CONFIRMED 状态申请取消
U->>V: 点击"申请取消"
V->>A: POST /api/appointments/{id}/cancel
A->>B: 校验状态为 CONFIRMED
B->>B: status → CANCEL_REQUESTED
B-->>A: success
A-->>V: ElMessage.success('取消申请已提交')
end
sequenceDiagram
actor A as Admin
participant V as AdminOrderListView
participant API as API Layer
participant B as Backend
Note over V: 页面加载
V->>API: GET /api/admin/appointments?pageNum=1&pageSize=10
API->>B: 分页查询所有预约
B-->>API: PageVo<AppointmentVo>
API-->>V: 表格渲染
Note over A,V: 审核通过
A->>V: 点击 PENDING 订单的"审核"
V->>V: 打开审核弹窗
A->>V: 选择"通过"
V->>API: PUT /api/admin/appointments/{id}/review<br/>{ action: 'CONFIRMED' }
API->>B: 校验订单
B->>B: status → CONFIRMED
B->>B: 批量插入 boarding_room_status (OCCUPIED)
B->>B: 关联 appointment_id
B-->>API: success
API-->>V: ElMessage.success
V->>V: 刷新列表
Note over A,V: 审核拒绝
A->>V: 选择"拒绝" + 填写原因
V->>API: PUT /api/admin/appointments/{id}/review<br/>{ action: 'CANCELLED', rejectReason }
API->>B: status → CANCELLED
B->>B: 保存 rejectReason
B-->>API: success
Note over A,V: 处理取消申请
A->>V: 点击 CANCEL_REQUESTED 的"处理取消"
V->>V: 打开处理弹窗
A->>V: 同意/拒绝
V->>API: PUT /api/admin/appointments/{id}/handle-cancel<br/>{ action: 'CANCELLED'|'CONFIRMED' }
API->>B: 更新 status + 释放房间
B-->>API: success
V->>V: 刷新列表