work.md 15 KB

客户端开发工作记录


阶段一:认证方案改造(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 配置一致)

    // 修改后的核心代码
    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. 状态结构从嵌套改为扁平:

    // 新状态结构
    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

export { useMetaStore } from "./meta.ts";
export { useUserStore } from "./user.ts";   // 改名

步骤 1.4 — 清理类型文件

文件:src/type/index.ts

操作:移除 LoginUser 接口中的 token?: string 字段(后续阶段六统一使用 Orval 生成类型,这里仅做最低限度清理)

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. 重写路由表,按详细设计定义所有路径:

    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:

    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 列表
  • 限制格式和大小

    <template>
    <el-upload
    :action="uploadUrl"
    :on-success="handleSuccess"
    :on-remove="handleRemove"
    :file-list="fileList"
    list-type="picture-card"
    :multiple="multiple"
    :limit="limit"
    :before-upload="beforeUpload"
    >
    <el-icon><Plus /></el-icon>
    </el-upload>
    </template>
    

步骤 5.2 — 空状态组件

文件:src/components/EmptyState.vue

功能:

  • 统一的空状态占位
  • 可配置图标、提示文字、操作按钮

阶段六:Orval 重新生成 API

步骤 6.1 — 启动后端

cd server && mvn spring-boot:run

确认 http://localhost:8080/api/v3/api-docs 可访问。


步骤 6.2 — 运行 Orval 生成

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. 替换为:

    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body, #app { height: 100%; }
    
  3. HTML 文件添加 viewport 设置:

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    

步骤 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