# 客户端开发工作记录 --- ## 阶段一:认证方案改造(Session 替换 Token) ### 步骤 1.1 — 修改 axios 实例 **文件**:`src/util/axios-instance.ts` **操作**: 1. 移除 request 拦截器中从 `localStorage` 读取 token 并设置 `Authorization` header 的逻辑 2. 新增 `withCredentials: true`(携带 Session Cookie) 3. 响应拦截器中的 401 跳转 `/login` 逻辑保留 4. 实例名保持 `customAxiosInstance`(与 orval.config.ts 的 mutator 配置一致) ```typescript // 修改后的核心代码 const axiosInstance: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 30000, withCredentials: true, // 携带 Session Cookie headers: { 'Content-Type': 'application/json', }, }); // 请求拦截器 — 移除 token 逻辑,仅保留必要处理 axiosInstance.interceptors.request.use( (config) => { return config; }, (error) => Promise.reject(error) ); // 响应拦截器 — 根据后端 Result 格式调整 axiosInstance.interceptors.response.use( (response: AxiosResponse) => { const { code, message } = response.data; if (code === 200) { return response.data; } return Promise.reject(new Error(message || '请求失败')); }, (error) => { if (error.response?.status === 401) { window.location.href = '/login'; } return Promise.reject(error); } ); ``` --- ### 步骤 1.2 — 重构用户 Store **文件**:`src/store/user.ts` **操作**: 1. 重命名 `useLoginUserStore` → `useUserStore` 2. 状态结构从嵌套改为扁平: ```typescript // 新状态结构 const state = reactive({ userId: null as number | null, nickname: '', avatarUrl: '', role: '', authStatus: '', isLoggedIn: false, }); ``` 3. Actions 重命名: - `loginSuccess(user)` → `login(user)`:登录成功后设置用户信息 - `logoutUser()` → `logout()`:清空状态 - `updateUser(user)` → `updateProfile(profile)`:更新资料 4. 移除 `localStorage.removeItem('token')` 调用 5. Getters:`isAuthenticated`(authStatus === 'AUTHENTICATED')、`isAdmin`(role === 'ADMIN') **更新 import 引用**:`store/index.ts`、`layout/UserLayout.vue`、`layout/AdminLayout.vue`、`router/index.ts`、`Login.vue` --- ### 步骤 1.3 — 更新 store 导出 **文件**:`src/store/index.ts` ```typescript export { useMetaStore } from "./meta.ts"; export { useUserStore } from "./user.ts"; // 改名 ``` --- ### 步骤 1.4 — 清理类型文件 **文件**:`src/type/index.ts` **操作**:移除 `LoginUser` 接口中的 `token?: string` 字段(后续阶段六统一使用 Orval 生成类型,这里仅做最低限度清理) ```typescript export interface User { id: string, name: string, avatar?: string role: 'admin' | 'user' | 'guest' } export interface LoginUser { isLogin: boolean, // token?: string ← 移除 user?: User } ``` --- ## 阶段二:布局与路由改造 ### 步骤 2.1 — 重写路由表 **文件**:`src/router/index.ts` **操作**: 1. 重写路由表,按详细设计定义所有路径: ```typescript const routes = [ // 公开页面 { path: '/login', name: 'login', component: () => import('@/view/Login.vue'), meta: { noAuth: true } }, { path: '/', redirect: '/home' }, { path: '/home', name: 'home', component: () => import('@/view/Home.vue') }, { path: '/item/:id', name: 'itemDetail', component: () => import('@/view/ItemDetail.vue') }, { path: '/search', name: 'search', component: () => import('@/view/Search.vue') }, // C端 — 需登录 { path: '/publish', name: 'publish', component: () => import('@/view/Publish.vue'), meta: { requiresAuth: true } }, { path: '/my/items', name: 'myItems', component: () => import('@/view/MyItems.vue'), meta: { requiresAuth: true } }, { path: '/my/orders', name: 'myOrders', component: () => import('@/view/MyOrders.vue'), meta: { requiresAuth: true } }, { path: '/order/:id', name: 'orderDetail', component: () => import('@/view/OrderDetail.vue'), meta: { requiresAuth: true } }, { path: '/messages', name: 'messages', component: () => import('@/view/Messages.vue'), meta: { requiresAuth: true } }, { path: '/messages/:userId', name: 'chat', component: () => import('@/view/Chat.vue'), meta: { requiresAuth: true } }, { path: '/profile', name: 'profile', component: () => import('@/view/Profile.vue'), meta: { requiresAuth: true } }, { path: '/profile/password', name: 'changePassword', component: () => import('@/view/ChangePassword.vue'), meta: { requiresAuth: true } }, // B端 — 需管理员权限 { path: '/admin/login', name: 'adminLogin', component: () => import('@/view/admin/Login.vue'), meta: { noAuth: true } }, { path: '/admin', redirect: '/admin/review/user' }, { path: '/admin/review/user', name: 'reviewUser', component: () => import('@/view/admin/ReviewUser.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, { path: '/admin/review/item', name: 'reviewItem', component: () => import('@/view/admin/ReviewItem.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, { path: '/admin/users', name: 'userManage', component: () => import('@/view/admin/UserManage.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, { path: '/admin/categories', name: 'categoryManage', component: () => import('@/view/admin/CategoryManage.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, ]; ``` 2. 路由守卫改用 `useUserStore`: ```typescript router.beforeEach((to, _from, next) => { const userStore = useUserStore(); if (to.meta.requiresAdmin) { if (!userStore.isLoggedIn) { next({ path: '/login', query: { redirect: to.fullPath } }); } else if (!userStore.isAdmin) { next('/'); } else { next(); } } else if (to.meta.requiresAuth) { if (!userStore.isLoggedIn) { next({ path: '/login', query: { redirect: to.fullPath } }); } else { next(); } } else { next(); } }); ``` --- ### 步骤 2.2 — 更新用户布局 **文件**:`src/layout/UserLayout.vue` **操作**: 1. 所有 `useLoginUserStore` 引用改为 `useUserStore` 2. 访问路径适配扁平结构:`loginUserStore.loginUser.isLogin` → `userStore.isLoggedIn` 3. 退出登录改为调用 `userStore.logout()`,移除 `localStorage.removeItem('token')` 4. 保留 Logo、标题、导航菜单 5. 新增底部 TabBar(移动端):首页 / 发布 / 消息 / 我的 --- ### 步骤 2.3 — 更新管理后台布局 **文件**:`src/layout/AdminLayout.vue` **操作**: 1. 适配新的扁平 store 结构 2. 侧边栏菜单改为: - 实名审核(`/admin/review/user`) - 物品审核(`/admin/review/item`) - 用户管理(`/admin/users`) - 分类管理(`/admin/categories`) --- ### 步骤 2.4 — 视图文件重命名 | 原文件名 | 新文件名 | 操作 | |---------|---------|------| | `view/LoginView.vue` | `view/Login.vue` | 重命名 + 更新 router import | | `view/HomeView.vue` | `view/Home.vue` | 重命名 + 更新 router import | | `view/AboutView.vue` | — | 删除(设计文档无此页面) | | `view/UserView.vue` | `view/admin/UserManage.vue` | 移动并重命名,后续适配新 API | --- ## 阶段三:核心页面开发 ### 步骤 3.1 — 首页 Home.vue **文件**:`src/view/Home.vue` **功能**: - 搜索栏(el-input,支持回车搜索跳转 `/search`) - 分类 Tabs(el-tabs,从 API 加载分类列表) - 物品网格(el-row/el-col,响应式:xs=24/sm=12/md=8/lg=6) - 物品卡片(el-card):图片 + 标题 + 价格 + 卖家昵称 - 分页(el-pagination) - 使用 Orval 生成的 `getItems()`、`getCategories()` API --- ### 步骤 3.2 — 物品详情 ItemDetail.vue **文件**:`src/view/ItemDetail.vue` **功能**: - 物品图片轮播(el-carousel) - 物品标题、价格、描述、交易方式 - 卖家信息(头像、昵称) - 操作按钮: - "联系卖家" → 跳转聊天 - "我想要" → 创建订单(需登录,弹窗确认) --- ### 步骤 3.3 — 搜索 Search.vue **文件**:`src/view/Search.vue` **功能**: - 搜索框(从 URL query 读取初始 keyword) - 分类筛选下拉 - 搜索结果列表(与首页一致) - 分页 --- ### 步骤 3.4 — 发布物品 Publish.vue **文件**:`src/view/Publish.vue` **功能**: - 表单:标题、描述、价格、分类选择、交易方式(el-radio-group) - 图片上传组件(使用步骤 5.1 的 ImageUploader) - 提交按钮 → 调用 `publishItem()`(multipart/form-data) --- ### 步骤 3.5 — 我的发布 MyItems.vue **文件**:`src/view/MyItems.vue` **功能**: - Tabs:售卖中 / 已下架 / 已售出 - 物品列表(每个带图片缩略图) - 操作:下架(售卖中)、删除 --- ### 步骤 3.6 — 我的订单 MyOrders.vue **文件**:`src/view/MyOrders.vue` **功能**: - Tabs:我买到的 / 我卖出的 - 订单卡片列表:物品缩略图 + 标题 + 价格 + 状态标签 - 订单状态按设计文档的映射: - 待接单 → 黄色标签 - 待面交 → 蓝色标签 - 已完成 → 绿色标签 - 已取消 → 灰色标签 --- ### 步骤 3.7 — 订单详情 OrderDetail.vue **文件**:`src/view/OrderDetail.vue` **功能**: - 订单信息卡片(物品信息、交易方式、创建时间) - 状态标签(el-tag,按状态显示不同颜色) - 时间线(el-timeline)展示状态变更 - 操作按钮(按状态和角色条件渲染): - 卖家视角:待接单 → 接单/拒单;待面交 → 确认交货 - 买家视角:待收货 → 确认收货 - "联系对方"按钮 --- ### 步骤 3.8 — 消息列表 Messages.vue **文件**:`src/view/Messages.vue` **功能**: - 会话列表:对方头像 + 昵称 + 最后一条消息 + 时间 + 未读红点 - 点击进入聊天(`/messages/{userId}`) - 使用 Orval 生成的 `getConversations()` API --- ### 步骤 3.9 — 聊天 Chat.vue **文件**:`src/view/Chat.vue` **功能**: - 消息气泡列表(自己的在右侧,对方的在左侧) - 输入区:文本输入框 + 图片上传按钮 - 发送按钮 - 自动滚动到底部 - 使用 Orval 生成的 `getMessageHistory()`、`sendMessage()` API - 进入时标记已读 --- ### 步骤 3.10 — 个人资料 + 修改密码 **文件**:`src/view/Profile.vue`、`src/view/ChangePassword.vue` **Profile.vue**: - 头像展示 + 修改(点击上传) - 昵称编辑 - 个性签名编辑 - 保存按钮 → 调用 `updateProfile()`(multipart/form-data) **ChangePassword.vue**: - 旧密码输入 - 新密码 + 确认新密码 - 提交 → 调用 `changePassword()` --- ## 阶段四:管理后台页面开发 ### 步骤 4.1 — 实名审核 ReviewUser.vue **文件**:`src/view/admin/ReviewUser.vue` **功能**: - 待审核用户列表(分页表格) - 每行显示:学号、姓名、昵称、注册时间 - 操作:通过 / 驳回 - 驳回时弹出对话框填写原因 - 可点击查看校园卡照片(el-image 大图预览) --- ### 步骤 4.2 — 物品审核 ReviewItem.vue **文件**:`src/view/admin/ReviewItem.vue` **功能**: - 待审核物品列表(分页表格) - 每行显示:标题、价格、卖家、发布时间 - 操作:通过上架 / 驳回下架 - 可点击查看物品详情和图片 --- ### 步骤 4.3 — 分类管理 CategoryManage.vue **文件**:`src/view/admin/CategoryManage.vue` **功能**: - 分类列表(带排序) - 新增分类对话框(名称 + 排序) - 编辑分类 - 启用/禁用开关(el-switch) --- ### 步骤 4.4 — 用户管理适配 UserManage.vue **文件**:`src/view/admin/UserManage.vue` **操作**: - 将原 `UserView.vue` 迁移至此 - 适配新的 Orval 生成的 API(使用 `getUserManageController()`) - 功能保留:用户列表、搜索、新增/编辑(简化,改为管理员视角)、封禁/解封、重置密码 --- ## 阶段五:通用组件开发 ### 步骤 5.1 — 图片上传组件 **文件**:`src/components/ImageUploader.vue` **功能**: - 封装 el-upload - 支持多图上传 - 图片预览(点击放大) - 图片删除 - 返回 URL 列表 - 限制格式和大小 ```vue ``` --- ### 步骤 5.2 — 空状态组件 **文件**:`src/components/EmptyState.vue` **功能**: - 统一的空状态占位 - 可配置图标、提示文字、操作按钮 --- ## 阶段六:Orval 重新生成 API ### 步骤 6.1 — 启动后端 ```bash cd server && mvn spring-boot:run ``` 确认 `http://localhost:8080/api/v3/api-docs` 可访问。 --- ### 步骤 6.2 — 运行 Orval 生成 ```bash cd ui && npm run api:gen ``` 生成产出: - `src/api/auth-controller.ts`(认证模块) - `src/api/user-controller.ts`(用户模块) - `src/api/item-controller.ts`(物品模块) - `src/api/order-controller.ts`(订单模块) - `src/api/message-controller.ts`(消息模块) - `src/api/review-controller.ts`(审核模块) - `src/api/user-manage-controller.ts`(用户管理) - `src/api/category-controller.ts`(分类管理) - `src/api/models/`(全部 TypeScript 类型) --- ### 步骤 6.3 — 统一类型系统 **操作**: 1. 删除 `src/type/index.ts` 中的手动接口 2. 所有组件改用 `import type { UserVO, ItemVO, OrderVO } from '@/api/models'` 3. 更新所有 API 调用方式为 Orval 工厂模式 --- ## 阶段七:集成与样式适配 ### 步骤 7.1 — 全局样式清理 **文件**:`src/style.css` **操作**: 1. 移除 Vite 默认样式(暗色主题、`#app` 居中布局、`a` 标签颜色等) 2. 替换为: ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body, #app { height: 100%; } ``` 3. HTML 文件添加 viewport 设置: ```html ``` --- ### 步骤 7.2 — 统一 loading 和错误处理 确保每个页面: - 数据加载时显示 el-skeleton 或 v-loading - API 错误时使用 ElMessage 提示(由 axios 拦截器兜底,页面层也可补充) - 列表无数据时使用 EmptyState 组件 --- ### 步骤 7.3 — 移动端兼容验证 使用 Chrome DevTools 验证: - 375px(iPhone SE):底部 TabBar 可点、卡片占满宽度、弹窗适配 - 768px(iPad):布局正常、导航折叠 - 1440px(桌面):多列网格、侧边栏正常 --- ## 各阶段文件变更汇总 | 阶段 | 新建文件 | 修改文件 | 删除文件 | |------|---------|---------|---------| | 一 | 0 | `axios-instance.ts`, `store/user.ts`, `store/index.ts`, `type/index.ts` | 0 | | 二 | 0 | `router/index.ts`, `UserLayout.vue`, `AdminLayout.vue` | `AboutView.vue` | | 三 | `Home.vue`, `ItemDetail.vue`, `Search.vue`, `Publish.vue`, `MyItems.vue`, `MyOrders.vue`, `OrderDetail.vue`, `Messages.vue`, `Chat.vue`, `Profile.vue`, `ChangePassword.vue` | 0 | 0 | | 四 | `ReviewUser.vue`, `ReviewItem.vue`, `CategoryManage.vue` | `UserManage.vue`(迁移) | `UserView.vue` | | 五 | `ImageUploader.vue`, `EmptyState.vue` | 0 | 0 | | 六 | 0 | Orval 自动覆盖生成 | `type/index.ts`(部分) | | 七 | 0 | `style.css`, `index.html` | 0 | | **合计** | **16** | **11** | **2** |