# 客户端开发工作记录
---
## 阶段一:认证方案改造(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** |