Explorar el Código

feat:基础页面开发完成,并对接服务端API

yangyi hace 4 meses
padre
commit
baab0bd5a5
Se han modificado 100 ficheros con 4393 adiciones y 1505 borrados
  1. 21 42
      orval.config.ts
  2. 0 41
      src/api/auth-controller.ts
  3. 85 0
      src/api/endpoints/auth.ts
  4. 72 0
      src/api/endpoints/categories.ts
  5. 153 0
      src/api/endpoints/items.ts
  6. 59 0
      src/api/endpoints/messages.ts
  7. 108 0
      src/api/endpoints/orders.ts
  8. 73 0
      src/api/endpoints/reviews.ts
  9. 62 0
      src/api/endpoints/user-management.ts
  10. 50 0
      src/api/endpoints/users.ts
  11. 0 23
      src/api/models/authTokenVo.ts
  12. 11 0
      src/api/models/boughtParams.ts
  13. 11 0
      src/api/models/byCategoryParams.ts
  14. 17 0
      src/api/models/category.ts
  15. 19 0
      src/api/models/categoryRequest.ts
  16. 22 0
      src/api/models/changePasswordRequest.ts
  17. 26 0
      src/api/models/conversationVO.ts
  18. 14 0
      src/api/models/createOrderRequest.ts
  19. 0 11
      src/api/models/deleteByIdParams.ts
  20. 0 18
      src/api/models/fieldError.ts
  21. 11 0
      src/api/models/historyParams.ts
  22. 45 21
      src/api/models/index.ts
  23. 31 0
      src/api/models/itemPublishRequest.ts
  24. 19 0
      src/api/models/itemPublishRequestTradeMethod.ts
  25. 36 0
      src/api/models/itemVO.ts
  26. 11 0
      src/api/models/list2Params.ts
  27. 11 0
      src/api/models/listParams.ts
  28. 8 9
      src/api/models/loginRequest.ts
  29. 34 0
      src/api/models/messageVO.ts
  30. 38 0
      src/api/models/orderVO.ts
  31. 23 0
      src/api/models/pageResultItemVO.ts
  32. 23 0
      src/api/models/pageResultMessageVO.ts
  33. 23 0
      src/api/models/pageResultOrderVO.ts
  34. 23 0
      src/api/models/pageResultUser.ts
  35. 0 18
      src/api/models/pageVoListUserVo.ts
  36. 11 0
      src/api/models/pendingItemsParams.ts
  37. 11 0
      src/api/models/pendingUsersParams.ts
  38. 0 16
      src/api/models/queryByPageParams.ts
  39. 0 31
      src/api/models/registerDto.ts
  40. 29 0
      src/api/models/registerRequest.ts
  41. 0 19
      src/api/models/response.ts
  42. 0 20
      src/api/models/responseAuthTokenVo.ts
  43. 0 19
      src/api/models/responseBoolean.ts
  44. 0 20
      src/api/models/responseListFieldError.ts
  45. 0 20
      src/api/models/responsePageVoListUserVo.ts
  46. 0 20
      src/api/models/responseUserVo.ts
  47. 19 0
      src/api/models/resultItemVO.ts
  48. 19 0
      src/api/models/resultListCategory.ts
  49. 19 0
      src/api/models/resultListConversationVO.ts
  50. 19 0
      src/api/models/resultListItemVO.ts
  51. 18 0
      src/api/models/resultLong.ts
  52. 19 0
      src/api/models/resultPageResultItemVO.ts
  53. 19 0
      src/api/models/resultPageResultMessageVO.ts
  54. 19 0
      src/api/models/resultPageResultOrderVO.ts
  55. 19 0
      src/api/models/resultPageResultUser.ts
  56. 19 0
      src/api/models/resultUserProfileVO.ts
  57. 18 0
      src/api/models/resultVoid.ts
  58. 20 0
      src/api/models/reviewRequest.ts
  59. 19 0
      src/api/models/reviewRequestResult.ts
  60. 12 0
      src/api/models/searchParams.ts
  61. 20 0
      src/api/models/sendMessageRequest.ts
  62. 11 0
      src/api/models/soldParams.ts
  63. 17 0
      src/api/models/statusRequest.ts
  64. 0 24
      src/api/models/updateUserAvatarDto.ts
  65. 0 30
      src/api/models/updateUserPasswordDto.ts
  66. 0 25
      src/api/models/updateUserStatusDto.ts
  67. 25 0
      src/api/models/user.ts
  68. 0 39
      src/api/models/userDto.ts
  69. 18 0
      src/api/models/userProfileRequest.ts
  70. 32 0
      src/api/models/userProfileVO.ts
  71. 0 25
      src/api/models/userVo.ts
  72. 0 117
      src/api/user-controller.ts
  73. 36 0
      src/components/EmptyState.vue
  74. 65 0
      src/components/ImageUploader.vue
  75. 96 96
      src/layout/AdminLayout.vue
  76. 81 42
      src/layout/UserLayout.vue
  77. 150 95
      src/router/index.ts
  78. 2 2
      src/store/index.ts
  79. 53 48
      src/store/user.ts
  80. 19 72
      src/style.css
  81. 0 11
      src/type/index.ts
  82. 29 48
      src/util/axios-instance.ts
  83. 0 14
      src/view/AboutView.vue
  84. 108 0
      src/view/ChangePassword.vue
  85. 182 0
      src/view/Chat.vue
  86. 212 0
      src/view/Home.vue
  87. 0 12
      src/view/HomeView.vue
  88. 163 0
      src/view/ItemDetail.vue
  89. 20 21
      src/view/Login.vue
  90. 105 0
      src/view/Messages.vue
  91. 185 0
      src/view/MyItems.vue
  92. 178 0
      src/view/MyOrders.vue
  93. 275 0
      src/view/OrderDetail.vue
  94. 142 0
      src/view/Profile.vue
  95. 143 0
      src/view/Publish.vue
  96. 123 0
      src/view/Register.vue
  97. 203 0
      src/view/Search.vue
  98. 0 436
      src/view/UserView.vue
  99. 157 0
      src/view/admin/CategoryManage.vue
  100. 95 0
      src/view/admin/Login.vue

+ 21 - 42
orval.config.ts

@@ -1,46 +1,25 @@
-import { defineConfig } from 'orval';
+import { defineConfig } from 'orval'
 
 
 export default defineConfig({
 export default defineConfig({
-    'your-api': {
-        // OpenAPI 规范文件路径(支持本地文件或 URL)
-        input: {
-            target: 'http://localhost:8080/v3/api-docs', // 可改为你的 Swagger JSON 地址,如:http://localhost:8000/openapi.json
-        },
-        output: {
-            // 生成代码的模式:tags-按接口标签拆分文件
-            mode: 'tags',
-
-            // 客户端类型:使用 axios
-            client: 'axios',
-
-            // 生成 vue-query hooks
-            target: './src/api/client.ts',
-
-            // 生成的 TypeScript 类型存放目录
-            schemas: './src/api/models',
-
-            // 每次生成前自动清理旧文件
-            clean: true,
-
-            // 覆盖默认配置
-            override: {
-                // 自定义 axios 实例
-                mutator: {
-                    path: './src/util/axios-instance.ts',
-                    name: 'customAxiosInstance',
-                },
-
-                // 为每个接口生成 vue-query hooks
-                query: {
-                    useQuery: true,      // 生成 useGetXXX hooks
-                    useMutation: true,   // 生成 usePostXXX hooks
-                },
-            },
-        },
-
-        // 生成后自动格式化代码
-        hooks: {
-            afterAllFilesWrite: 'prettier --write',
+  campusTrade: {
+    input: {
+      target: 'http://localhost:8090/v3/api-docs',
+    },
+    output: {
+      mode: 'tags',
+      client: 'axios',
+      target: './src/api/endpoints/',
+      schemas: './src/api/models',
+      clean: true,
+      override: {
+        mutator: {
+          path: './src/util/axios-instance.ts',
+          name: 'customAxiosInstance',
         },
         },
+      },
+    },
+    hooks: {
+      afterAllFilesWrite: 'prettier --write',
     },
     },
-});
+  },
+})

+ 0 - 41
src/api/auth-controller.ts

@@ -1,41 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-import type { LoginDto, RegisterDto, ResponseAuthTokenVo } from "./models";
-import { customAxiosInstance } from "../util/axios-instance";
-
-export const getAuthController = () => {
-  /**
-   * @summary 用户注册
-   */
-  const register = (registerDto: RegisterDto) => {
-    return customAxiosInstance<ResponseAuthTokenVo>({
-      url: `/auth/register`,
-      method: "POST",
-      headers: { "Content-Type": "application/json" },
-      data: registerDto,
-    });
-  };
-  /**
-   * @summary 用户登录
-   */
-  const login = (loginDto: LoginDto) => {
-    return customAxiosInstance<ResponseAuthTokenVo>({
-      url: `/auth/login`,
-      method: "POST",
-      headers: { "Content-Type": "application/json" },
-      data: loginDto,
-    });
-  };
-  return { register, login };
-};
-export type RegisterResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getAuthController>["register"]>>
->;
-export type LoginResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getAuthController>["login"]>>
->;

+ 85 - 0
src/api/endpoints/auth.ts

@@ -0,0 +1,85 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type {
+  ChangePasswordRequest,
+  LoginRequest,
+  RegisterRequest,
+  ResultLong,
+  ResultUserProfileVO,
+  ResultVoid,
+} from "../models";
+import { customAxiosInstance } from "../../util/axios-instance";
+
+export const getAuth = () => {
+  /**
+   * 学号+姓名+密码+校园卡照片,提交后进入待审核状态
+   * @summary 用户注册
+   */
+  const register = (registerRequest: RegisterRequest) => {
+    const formData = new FormData();
+    formData.append("studentId", registerRequest.studentId);
+    formData.append("name", registerRequest.name);
+    formData.append("password", registerRequest.password);
+    if (registerRequest.campusCardImage !== undefined) {
+      formData.append("campusCardImage", registerRequest.campusCardImage);
+    }
+
+    return customAxiosInstance<ResultLong>({
+      url: `/auth/register`,
+      method: "POST",
+      headers: { "Content-Type": "multipart/form-data" },
+      data: formData,
+    });
+  };
+  /**
+   * 销毁当前 Session
+   * @summary 用户登出
+   */
+  const logout = () => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/auth/logout`,
+      method: "POST",
+    });
+  };
+  /**
+   * 学号+密码登录,创建 Session 会话
+   * @summary 用户登录
+   */
+  const login = (loginRequest: LoginRequest) => {
+    return customAxiosInstance<ResultUserProfileVO>({
+      url: `/auth/login`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: loginRequest,
+    });
+  };
+  /**
+   * 需提供旧密码验证
+   * @summary 修改密码
+   */
+  const changePassword = (changePasswordRequest: ChangePasswordRequest) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/auth/change-password`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: changePasswordRequest,
+    });
+  };
+  return { register, logout, login, changePassword };
+};
+export type RegisterResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getAuth>["register"]>>
+>;
+export type LogoutResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getAuth>["logout"]>>
+>;
+export type LoginResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getAuth>["login"]>>
+>;
+export type ChangePasswordResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getAuth>["changePassword"]>>
+>;

+ 72 - 0
src/api/endpoints/categories.ts

@@ -0,0 +1,72 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type {
+  CategoryRequest,
+  ResultListCategory,
+  ResultLong,
+  ResultVoid,
+  StatusRequest,
+} from "../models";
+import { customAxiosInstance } from "../../util/axios-instance";
+
+export const getCategories = () => {
+  /**
+   * @summary 修改分类
+   */
+  const update = (id: number, categoryRequest: CategoryRequest) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/admin/categories/${id}`,
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      data: categoryRequest,
+    });
+  };
+  /**
+   * @summary 启用/禁用分类
+   */
+  const toggleStatus = (id: number, statusRequest: StatusRequest) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/admin/categories/${id}/status`,
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      data: statusRequest,
+    });
+  };
+  /**
+   * @summary 分类列表
+   */
+  const list1 = () => {
+    return customAxiosInstance<ResultListCategory>({
+      url: `/admin/categories`,
+      method: "GET",
+    });
+  };
+  /**
+   * @summary 新增分类
+   */
+  const create1 = (categoryRequest: CategoryRequest) => {
+    return customAxiosInstance<ResultLong>({
+      url: `/admin/categories`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: categoryRequest,
+    });
+  };
+  return { update, toggleStatus, list1, create1 };
+};
+export type UpdateResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getCategories>["update"]>>
+>;
+export type ToggleStatusResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getCategories>["toggleStatus"]>>
+>;
+export type List1Result = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getCategories>["list1"]>>
+>;
+export type Create1Result = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getCategories>["create1"]>>
+>;

+ 153 - 0
src/api/endpoints/items.ts

@@ -0,0 +1,153 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type {
+  ByCategoryParams,
+  ItemPublishRequest,
+  ListParams,
+  ResultItemVO,
+  ResultListItemVO,
+  ResultLong,
+  ResultPageResultItemVO,
+  ResultVoid,
+  SearchParams,
+} from "../models";
+import { customAxiosInstance } from "../../util/axios-instance";
+
+export const getItems = () => {
+  /**
+   * 卖家下架自己的物品
+   * @summary 下架物品
+   */
+  const offline = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/items/${id}/offline`,
+      method: "PUT",
+    });
+  };
+  /**
+   * 按发布时间倒序,分页返回
+   * @summary 首页物品列表
+   */
+  const list = (params?: ListParams) => {
+    return customAxiosInstance<ResultPageResultItemVO>({
+      url: `/items`,
+      method: "GET",
+      params,
+    });
+  };
+  /**
+   * 需要实名认证,图片以 multipart 上传
+   * @summary 发布物品
+   */
+  const publish = (itemPublishRequest: ItemPublishRequest) => {
+    const formData = new FormData();
+    formData.append("title", itemPublishRequest.title);
+    if (itemPublishRequest.description !== undefined) {
+      formData.append("description", itemPublishRequest.description);
+    }
+    formData.append("price", itemPublishRequest.price.toString());
+    if (itemPublishRequest.categoryId !== undefined) {
+      formData.append("categoryId", itemPublishRequest.categoryId.toString());
+    }
+    formData.append("tradeMethod", itemPublishRequest.tradeMethod);
+    if (itemPublishRequest.images !== undefined) {
+      itemPublishRequest.images.forEach((value) =>
+        formData.append("images", value),
+      );
+    }
+
+    return customAxiosInstance<ResultLong>({
+      url: `/items`,
+      method: "POST",
+      headers: { "Content-Type": "multipart/form-data" },
+      data: formData,
+    });
+  };
+  /**
+   * @summary 物品详情
+   */
+  const detail = (id: number) => {
+    return customAxiosInstance<ResultItemVO>({
+      url: `/items/${id}`,
+      method: "GET",
+    });
+  };
+  /**
+   * 卖家删除自己的物品
+   * @summary 删除物品
+   */
+  const _delete = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/items/${id}`,
+      method: "DELETE",
+    });
+  };
+  /**
+   * 按标题/描述模糊匹配
+   * @summary 搜索物品
+   */
+  const search = (params: SearchParams) => {
+    return customAxiosInstance<ResultPageResultItemVO>({
+      url: `/items/search`,
+      method: "GET",
+      params,
+    });
+  };
+  /**
+   * @summary 我的发布列表
+   */
+  const myItems = () => {
+    return customAxiosInstance<ResultListItemVO>({
+      url: `/items/my`,
+      method: "GET",
+    });
+  };
+  /**
+   * @summary 按分类筛选
+   */
+  const byCategory = (id: number, params?: ByCategoryParams) => {
+    return customAxiosInstance<ResultPageResultItemVO>({
+      url: `/items/category/${id}`,
+      method: "GET",
+      params,
+    });
+  };
+  return {
+    offline,
+    list,
+    publish,
+    detail,
+    _delete,
+    search,
+    myItems,
+    byCategory,
+  };
+};
+export type OfflineResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getItems>["offline"]>>
+>;
+export type ListResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getItems>["list"]>>
+>;
+export type PublishResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getItems>["publish"]>>
+>;
+export type DetailResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getItems>["detail"]>>
+>;
+export type _DeleteResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getItems>["_delete"]>>
+>;
+export type SearchResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getItems>["search"]>>
+>;
+export type MyItemsResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getItems>["myItems"]>>
+>;
+export type ByCategoryResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getItems>["byCategory"]>>
+>;

+ 59 - 0
src/api/endpoints/messages.ts

@@ -0,0 +1,59 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type {
+  HistoryParams,
+  ResultListConversationVO,
+  ResultLong,
+  ResultPageResultMessageVO,
+  SendMessageRequest,
+} from "../models";
+import { customAxiosInstance } from "../../util/axios-instance";
+
+export const getMessages = () => {
+  /**
+   * @summary 发送消息
+   */
+  const send = (sendMessageRequest: SendMessageRequest) => {
+    return customAxiosInstance<ResultLong>({
+      url: `/messages`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: sendMessageRequest,
+    });
+  };
+  /**
+   * 与指定用户的聊天记录,分页
+   * @summary 聊天记录
+   */
+  const history = (userId: number, params?: HistoryParams) => {
+    return customAxiosInstance<ResultPageResultMessageVO>({
+      url: `/messages/${userId}`,
+      method: "GET",
+      params,
+    });
+  };
+  /**
+   * 返回当前用户的所有会话及最后一条消息
+   * @summary 会话列表
+   */
+  const conversations = () => {
+    return customAxiosInstance<ResultListConversationVO>({
+      url: `/messages/conversations`,
+      method: "GET",
+    });
+  };
+  return { send, history, conversations };
+};
+export type SendResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getMessages>["send"]>>
+>;
+export type HistoryResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getMessages>["history"]>>
+>;
+export type ConversationsResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getMessages>["conversations"]>>
+>;

+ 108 - 0
src/api/endpoints/orders.ts

@@ -0,0 +1,108 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type {
+  BoughtParams,
+  CreateOrderRequest,
+  ResultLong,
+  ResultPageResultOrderVO,
+  ResultVoid,
+  SoldParams,
+} from "../models";
+import { customAxiosInstance } from "../../util/axios-instance";
+
+export const getOrders = () => {
+  /**
+   * @summary 卖家拒单
+   */
+  const reject = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/orders/${id}/reject`,
+      method: "PUT",
+    });
+  };
+  /**
+   * @summary 卖家确认交货
+   */
+  const deliver = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/orders/${id}/deliver`,
+      method: "PUT",
+    });
+  };
+  /**
+   * @summary 买家确认收货
+   */
+  const confirm = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/orders/${id}/confirm`,
+      method: "PUT",
+    });
+  };
+  /**
+   * @summary 卖家接单
+   */
+  const accept = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/orders/${id}/accept`,
+      method: "PUT",
+    });
+  };
+  /**
+   * 买家点击『我想要』生成订单
+   * @summary 生成订单
+   */
+  const create = (createOrderRequest: CreateOrderRequest) => {
+    return customAxiosInstance<ResultLong>({
+      url: `/orders`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: createOrderRequest,
+    });
+  };
+  /**
+   * @summary 我卖出的订单
+   */
+  const sold = (params?: SoldParams) => {
+    return customAxiosInstance<ResultPageResultOrderVO>({
+      url: `/orders/sold`,
+      method: "GET",
+      params,
+    });
+  };
+  /**
+   * @summary 我买到的订单
+   */
+  const bought = (params?: BoughtParams) => {
+    return customAxiosInstance<ResultPageResultOrderVO>({
+      url: `/orders/bought`,
+      method: "GET",
+      params,
+    });
+  };
+  return { reject, deliver, confirm, accept, create, sold, bought };
+};
+export type RejectResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getOrders>["reject"]>>
+>;
+export type DeliverResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getOrders>["deliver"]>>
+>;
+export type ConfirmResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getOrders>["confirm"]>>
+>;
+export type AcceptResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getOrders>["accept"]>>
+>;
+export type CreateResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getOrders>["create"]>>
+>;
+export type SoldResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getOrders>["sold"]>>
+>;
+export type BoughtResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getOrders>["bought"]>>
+>;

+ 73 - 0
src/api/endpoints/reviews.ts

@@ -0,0 +1,73 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type {
+  PendingItemsParams,
+  PendingUsersParams,
+  ResultPageResultItemVO,
+  ResultPageResultUser,
+  ResultVoid,
+  ReviewRequest,
+} from "../models";
+import { customAxiosInstance } from "../../util/axios-instance";
+
+export const getReviews = () => {
+  /**
+   * @summary 审核用户实名认证
+   */
+  const reviewUser = (id: number, reviewRequest: ReviewRequest) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/admin/review/user/${id}`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: reviewRequest,
+    });
+  };
+  /**
+   * @summary 审核物品上架
+   */
+  const reviewItem = (id: number, reviewRequest: ReviewRequest) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/admin/review/item/${id}`,
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      data: reviewRequest,
+    });
+  };
+  /**
+   * @summary 待审核用户列表
+   */
+  const pendingUsers = (params?: PendingUsersParams) => {
+    return customAxiosInstance<ResultPageResultUser>({
+      url: `/admin/review/users`,
+      method: "GET",
+      params,
+    });
+  };
+  /**
+   * @summary 待审核物品列表
+   */
+  const pendingItems = (params?: PendingItemsParams) => {
+    return customAxiosInstance<ResultPageResultItemVO>({
+      url: `/admin/review/items`,
+      method: "GET",
+      params,
+    });
+  };
+  return { reviewUser, reviewItem, pendingUsers, pendingItems };
+};
+export type ReviewUserResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getReviews>["reviewUser"]>>
+>;
+export type ReviewItemResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getReviews>["reviewItem"]>>
+>;
+export type PendingUsersResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getReviews>["pendingUsers"]>>
+>;
+export type PendingItemsResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getReviews>["pendingItems"]>>
+>;

+ 62 - 0
src/api/endpoints/user-management.ts

@@ -0,0 +1,62 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { List2Params, ResultPageResultUser, ResultVoid } from "../models";
+import { customAxiosInstance } from "../../util/axios-instance";
+
+export const getUserManagement = () => {
+  /**
+   * @summary 解封用户
+   */
+  const unban = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/admin/users/${id}/unban`,
+      method: "PUT",
+    });
+  };
+  /**
+   * @summary 封禁用户
+   */
+  const ban = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/admin/users/${id}/ban`,
+      method: "PUT",
+    });
+  };
+  /**
+   * 重置为默认密码 123456
+   * @summary 重置用户密码
+   */
+  const resetPassword = (id: number) => {
+    return customAxiosInstance<ResultVoid>({
+      url: `/admin/users/${id}/reset-password`,
+      method: "POST",
+    });
+  };
+  /**
+   * @summary 用户列表
+   */
+  const list2 = (params?: List2Params) => {
+    return customAxiosInstance<ResultPageResultUser>({
+      url: `/admin/users`,
+      method: "GET",
+      params,
+    });
+  };
+  return { unban, ban, resetPassword, list2 };
+};
+export type UnbanResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getUserManagement>["unban"]>>
+>;
+export type BanResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getUserManagement>["ban"]>>
+>;
+export type ResetPasswordResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getUserManagement>["resetPassword"]>>
+>;
+export type List2Result = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getUserManagement>["list2"]>>
+>;

+ 50 - 0
src/api/endpoints/users.ts

@@ -0,0 +1,50 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { ResultUserProfileVO, UserProfileRequest } from "../models";
+import { customAxiosInstance } from "../../util/axios-instance";
+
+export const getUsers = () => {
+  /**
+   * @summary 获取个人资料
+   */
+  const profile = () => {
+    return customAxiosInstance<ResultUserProfileVO>({
+      url: `/user/profile`,
+      method: "GET",
+    });
+  };
+  /**
+   * 支持修改昵称、个性签名、头像
+   * @summary 修改个人资料
+   */
+  const updateProfile = (userProfileRequest: UserProfileRequest) => {
+    const formData = new FormData();
+    if (userProfileRequest.nickname !== undefined) {
+      formData.append("nickname", userProfileRequest.nickname);
+    }
+    if (userProfileRequest.signature !== undefined) {
+      formData.append("signature", userProfileRequest.signature);
+    }
+    if (userProfileRequest.avatar !== undefined) {
+      formData.append("avatar", userProfileRequest.avatar);
+    }
+
+    return customAxiosInstance<ResultUserProfileVO>({
+      url: `/user/profile`,
+      method: "PUT",
+      headers: { "Content-Type": "multipart/form-data" },
+      data: formData,
+    });
+  };
+  return { profile, updateProfile };
+};
+export type ProfileResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getUsers>["profile"]>>
+>;
+export type UpdateProfileResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getUsers>["updateProfile"]>>
+>;

+ 0 - 23
src/api/models/authTokenVo.ts

@@ -1,23 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 登录响应
- */
-export interface AuthTokenVo {
-  /** 账号 */
-  account?: string;
-  /** 角色 */
-  role?: string;
-  /** JWT token */
-  token?: string;
-  /** 用户ID */
-  userId?: number;
-  /** 用户名 */
-  username?: string;
-}

+ 11 - 0
src/api/models/boughtParams.ts

@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type BoughtParams = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 11 - 0
src/api/models/byCategoryParams.ts

@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type ByCategoryParams = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 17 - 0
src/api/models/category.ts

@@ -0,0 +1,17 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 分类实体
+ */
+export interface Category {
+  createdAt?: string;
+  id?: number;
+  name?: string;
+  sortOrder?: number;
+  status?: string;
+}

+ 19 - 0
src/api/models/categoryRequest.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 分类请求
+ */
+export interface CategoryRequest {
+  /**
+   * 分类名称
+   * @minLength 1
+   */
+  name: string;
+  /** 排序 */
+  sortOrder?: number;
+}

+ 22 - 0
src/api/models/changePasswordRequest.ts

@@ -0,0 +1,22 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 修改密码请求
+ */
+export interface ChangePasswordRequest {
+  /**
+   * 新密码
+   * @minLength 1
+   */
+  newPassword: string;
+  /**
+   * 旧密码
+   * @minLength 1
+   */
+  oldPassword: string;
+}

+ 26 - 0
src/api/models/conversationVO.ts

@@ -0,0 +1,26 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 会话信息
+ */
+export interface ConversationVO {
+  /** 对方头像 */
+  avatarUrl?: string;
+  /** 最后一条消息内容 */
+  lastMessage?: string;
+  /** 最后一条消息图片 */
+  lastMessageImage?: string;
+  /** 最后消息时间 */
+  lastMessageTime?: string;
+  /** 对方昵称 */
+  nickname?: string;
+  /** 未读数 */
+  unreadCount?: number;
+  /** 对方用户ID */
+  userId?: number;
+}

+ 14 - 0
src/api/models/createOrderRequest.ts

@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 创建订单请求
+ */
+export interface CreateOrderRequest {
+  /** 物品ID */
+  itemId: number;
+}

+ 0 - 11
src/api/models/deleteByIdParams.ts

@@ -1,11 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-export type DeleteByIdParams = {
-  ids: string[];
-};

+ 0 - 18
src/api/models/fieldError.ts

@@ -1,18 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-export interface FieldError {
-  arguments?: unknown[];
-  bindingFailure?: boolean;
-  code?: string;
-  codes?: string[];
-  defaultMessage?: string;
-  field?: string;
-  objectName?: string;
-  rejectedValue?: unknown;
-}

+ 11 - 0
src/api/models/historyParams.ts

@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type HistoryParams = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 45 - 21
src/api/models/index.ts

@@ -1,26 +1,50 @@
 /**
 /**
  * Generated by orval v7.0.1 🍺
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
  * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
+ * OpenAPI definition
+ * OpenAPI spec version: v0
  */
  */
 
 
-export * from "./authTokenVo";
-export * from "./deleteByIdParams";
-export * from "./fieldError";
-export * from "./loginDto";
-export * from "./pageVoListUserVo";
-export * from "./queryByPageParams";
-export * from "./registerDto";
-export * from "./response";
-export * from "./responseAuthTokenVo";
-export * from "./responseBoolean";
-export * from "./responseListFieldError";
-export * from "./responsePageVoListUserVo";
-export * from "./responseUserVo";
-export * from "./updateUserAvatarDto";
-export * from "./updateUserPasswordDto";
-export * from "./updateUserStatusDto";
-export * from "./userDto";
-export * from "./userVo";
+export * from "./boughtParams";
+export * from "./byCategoryParams";
+export * from "./category";
+export * from "./categoryRequest";
+export * from "./changePasswordRequest";
+export * from "./conversationVO";
+export * from "./createOrderRequest";
+export * from "./historyParams";
+export * from "./itemPublishRequest";
+export * from "./itemPublishRequestTradeMethod";
+export * from "./itemVO";
+export * from "./list2Params";
+export * from "./listParams";
+export * from "./loginRequest";
+export * from "./messageVO";
+export * from "./orderVO";
+export * from "./pageResultItemVO";
+export * from "./pageResultMessageVO";
+export * from "./pageResultOrderVO";
+export * from "./pageResultUser";
+export * from "./pendingItemsParams";
+export * from "./pendingUsersParams";
+export * from "./registerRequest";
+export * from "./resultItemVO";
+export * from "./resultListCategory";
+export * from "./resultListConversationVO";
+export * from "./resultListItemVO";
+export * from "./resultLong";
+export * from "./resultPageResultItemVO";
+export * from "./resultPageResultMessageVO";
+export * from "./resultPageResultOrderVO";
+export * from "./resultPageResultUser";
+export * from "./resultUserProfileVO";
+export * from "./resultVoid";
+export * from "./reviewRequest";
+export * from "./reviewRequestResult";
+export * from "./searchParams";
+export * from "./sendMessageRequest";
+export * from "./soldParams";
+export * from "./statusRequest";
+export * from "./user";
+export * from "./userProfileRequest";
+export * from "./userProfileVO";

+ 31 - 0
src/api/models/itemPublishRequest.ts

@@ -0,0 +1,31 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { ItemPublishRequestTradeMethod } from "./itemPublishRequestTradeMethod";
+
+/**
+ * 发布物品请求(multipart/form-data)
+ */
+export interface ItemPublishRequest {
+  /** 分类ID */
+  categoryId?: number;
+  /** 描述 */
+  description?: string;
+  /** 物品图片列表 */
+  images?: Blob[];
+  /** 价格 */
+  price: number;
+  /**
+   * 标题
+   * @minLength 1
+   */
+  title: string;
+  /**
+   * 交易方式
+   * @minLength 1
+   */
+  tradeMethod: ItemPublishRequestTradeMethod;
+}

+ 19 - 0
src/api/models/itemPublishRequestTradeMethod.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 交易方式
+ * @minLength 1
+ */
+export type ItemPublishRequestTradeMethod =
+  (typeof ItemPublishRequestTradeMethod)[keyof typeof ItemPublishRequestTradeMethod];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const ItemPublishRequestTradeMethod = {
+  PICKUP: "PICKUP",
+  DELIVERY: "DELIVERY",
+} as const;

+ 36 - 0
src/api/models/itemVO.ts

@@ -0,0 +1,36 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 物品信息
+ */
+export interface ItemVO {
+  /** 分类名称 */
+  categoryName?: string;
+  /** 发布时间 */
+  createdAt?: string;
+  /** 描述 */
+  description?: string;
+  /** 物品ID */
+  id?: number;
+  /** 图片列表 */
+  images?: string[];
+  /** 价格 */
+  price?: number;
+  /** 卖家头像 */
+  sellerAvatar?: string;
+  /** 卖家ID */
+  sellerId?: number;
+  /** 卖家昵称 */
+  sellerNickname?: string;
+  /** 状态 */
+  status?: string;
+  /** 标题 */
+  title?: string;
+  /** 交易方式 */
+  tradeMethod?: string;
+}

+ 11 - 0
src/api/models/list2Params.ts

@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type List2Params = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 11 - 0
src/api/models/listParams.ts

@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type ListParams = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 8 - 9
src/api/models/loginDto.ts → src/api/models/loginRequest.ts

@@ -1,23 +1,22 @@
 /**
 /**
  * Generated by orval v7.0.1 🍺
  * Generated by orval v7.0.1 🍺
  * Do not edit manually.
  * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
+ * OpenAPI definition
+ * OpenAPI spec version: v0
  */
  */
 
 
 /**
 /**
- * 登录请求参数
+ * 登录请求
  */
  */
-export interface LoginDto {
+export interface LoginRequest {
   /**
   /**
-   * 账号
+   * 密码
    * @minLength 1
    * @minLength 1
    */
    */
-  account: string;
+  password: string;
   /**
   /**
-   * 密码
+   * 学号
    * @minLength 1
    * @minLength 1
    */
    */
-  password: string;
+  studentId: string;
 }
 }

+ 34 - 0
src/api/models/messageVO.ts

@@ -0,0 +1,34 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 消息信息
+ */
+export interface MessageVO {
+  /** 文本内容 */
+  content?: string;
+  /** 发送时间 */
+  createdAt?: string;
+  /** 发送方头像 */
+  fromUserAvatar?: string;
+  /** 发送方ID */
+  fromUserId?: number;
+  /** 发送方昵称 */
+  fromUserNickname?: string;
+  /** 消息ID */
+  id?: number;
+  /** 图片URL */
+  imageUrl?: string;
+  /** 是否已读 */
+  isRead?: boolean;
+  /** 接收方头像 */
+  toUserAvatar?: string;
+  /** 接收方ID */
+  toUserId?: number;
+  /** 接收方昵称 */
+  toUserNickname?: string;
+}

+ 38 - 0
src/api/models/orderVO.ts

@@ -0,0 +1,38 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 订单信息
+ */
+export interface OrderVO {
+  /** 买家ID */
+  buyerId?: number;
+  /** 买家昵称 */
+  buyerNickname?: string;
+  /** 创建时间 */
+  createdAt?: string;
+  /** 订单ID */
+  id?: number;
+  /** 物品ID */
+  itemId?: number;
+  /** 物品缩略图 */
+  itemImage?: string;
+  /** 物品价格 */
+  itemPrice?: number;
+  /** 物品标题 */
+  itemTitle?: string;
+  /** 卖家ID */
+  sellerId?: number;
+  /** 卖家昵称 */
+  sellerNickname?: string;
+  /** 订单状态 */
+  status?: string;
+  /** 交易方式 */
+  tradeMethod?: string;
+  /** 更新时间 */
+  updatedAt?: string;
+}

+ 23 - 0
src/api/models/pageResultItemVO.ts

@@ -0,0 +1,23 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { ItemVO } from "./itemVO";
+
+/**
+ * 分页结果
+ */
+export interface PageResultItemVO {
+  /** 数据列表 */
+  list?: ItemVO[];
+  /** 当前页码 */
+  pageNum?: number;
+  /** 总页数 */
+  pages?: number;
+  /** 每页条数 */
+  pageSize?: number;
+  /** 总记录数 */
+  total?: number;
+}

+ 23 - 0
src/api/models/pageResultMessageVO.ts

@@ -0,0 +1,23 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { MessageVO } from "./messageVO";
+
+/**
+ * 分页结果
+ */
+export interface PageResultMessageVO {
+  /** 数据列表 */
+  list?: MessageVO[];
+  /** 当前页码 */
+  pageNum?: number;
+  /** 总页数 */
+  pages?: number;
+  /** 每页条数 */
+  pageSize?: number;
+  /** 总记录数 */
+  total?: number;
+}

+ 23 - 0
src/api/models/pageResultOrderVO.ts

@@ -0,0 +1,23 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { OrderVO } from "./orderVO";
+
+/**
+ * 分页结果
+ */
+export interface PageResultOrderVO {
+  /** 数据列表 */
+  list?: OrderVO[];
+  /** 当前页码 */
+  pageNum?: number;
+  /** 总页数 */
+  pages?: number;
+  /** 每页条数 */
+  pageSize?: number;
+  /** 总记录数 */
+  total?: number;
+}

+ 23 - 0
src/api/models/pageResultUser.ts

@@ -0,0 +1,23 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { User } from "./user";
+
+/**
+ * 分页结果
+ */
+export interface PageResultUser {
+  /** 数据列表 */
+  list?: User[];
+  /** 当前页码 */
+  pageNum?: number;
+  /** 总页数 */
+  pages?: number;
+  /** 每页条数 */
+  pageSize?: number;
+  /** 总记录数 */
+  total?: number;
+}

+ 0 - 18
src/api/models/pageVoListUserVo.ts

@@ -1,18 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-import type { UserVo } from "./userVo";
-
-/**
- * 分页数据对象
- */
-export interface PageVoListUserVo {
-  /** 当前页的数据 */
-  data?: UserVo[];
-  /** 总数据条数 */
-  total?: number;
-}

+ 11 - 0
src/api/models/pendingItemsParams.ts

@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type PendingItemsParams = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 11 - 0
src/api/models/pendingUsersParams.ts

@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type PendingUsersParams = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 0 - 16
src/api/models/queryByPageParams.ts

@@ -1,16 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-export type QueryByPageParams = {
-  account: string;
-  username: string;
-  role: string;
-  enable?: number;
-  pageNum?: number;
-  pageSize?: number;
-};

+ 0 - 31
src/api/models/registerDto.ts

@@ -1,31 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 注册请求参数
- */
-export interface RegisterDto {
-  /**
-   * 账号
-   * @minLength 4
-   * @maxLength 20
-   */
-  account: string;
-  /**
-   * 密码
-   * @minLength 4
-   * @maxLength 20
-   */
-  password: string;
-  /**
-   * 用户名
-   * @minLength 2
-   * @maxLength 32
-   */
-  username: string;
-}

+ 29 - 0
src/api/models/registerRequest.ts

@@ -0,0 +1,29 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 注册请求(multipart/form-data)
+ */
+export interface RegisterRequest {
+  /** 校园卡照片文件 */
+  campusCardImage?: Blob;
+  /**
+   * 姓名
+   * @minLength 1
+   */
+  name: string;
+  /**
+   * 密码
+   * @minLength 1
+   */
+  password: string;
+  /**
+   * 学号
+   * @minLength 1
+   */
+  studentId: string;
+}

+ 0 - 19
src/api/models/response.ts

@@ -1,19 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 后端统一的响应实体
- */
-export interface Response {
-  /** 状态码;200:成功 */
-  code?: number;
-  /** 响应的具体数据 */
-  data?: unknown;
-  /** 响应附加信息 */
-  message?: string;
-}

+ 0 - 20
src/api/models/responseAuthTokenVo.ts

@@ -1,20 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-import type { AuthTokenVo } from "./authTokenVo";
-
-/**
- * 后端统一的响应实体
- */
-export interface ResponseAuthTokenVo {
-  /** 状态码;200:成功 */
-  code?: number;
-  /** 响应的具体数据 */
-  data?: AuthTokenVo;
-  /** 响应附加信息 */
-  message?: string;
-}

+ 0 - 19
src/api/models/responseBoolean.ts

@@ -1,19 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 后端统一的响应实体
- */
-export interface ResponseBoolean {
-  /** 状态码;200:成功 */
-  code?: number;
-  /** 响应的具体数据 */
-  data?: boolean;
-  /** 响应附加信息 */
-  message?: string;
-}

+ 0 - 20
src/api/models/responseListFieldError.ts

@@ -1,20 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-import type { FieldError } from "./fieldError";
-
-/**
- * 后端统一的响应实体
- */
-export interface ResponseListFieldError {
-  /** 状态码;200:成功 */
-  code?: number;
-  /** 响应的具体数据 */
-  data?: FieldError[];
-  /** 响应附加信息 */
-  message?: string;
-}

+ 0 - 20
src/api/models/responsePageVoListUserVo.ts

@@ -1,20 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-import type { PageVoListUserVo } from "./pageVoListUserVo";
-
-/**
- * 后端统一的响应实体
- */
-export interface ResponsePageVoListUserVo {
-  /** 状态码;200:成功 */
-  code?: number;
-  /** 响应的具体数据 */
-  data?: PageVoListUserVo;
-  /** 响应附加信息 */
-  message?: string;
-}

+ 0 - 20
src/api/models/responseUserVo.ts

@@ -1,20 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-import type { UserVo } from "./userVo";
-
-/**
- * 后端统一的响应实体
- */
-export interface ResponseUserVo {
-  /** 状态码;200:成功 */
-  code?: number;
-  /** 响应的具体数据 */
-  data?: UserVo;
-  /** 响应附加信息 */
-  message?: string;
-}

+ 19 - 0
src/api/models/resultItemVO.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { ItemVO } from "./itemVO";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultItemVO {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: ItemVO;
+  /** 消息 */
+  message?: string;
+}

+ 19 - 0
src/api/models/resultListCategory.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { Category } from "./category";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultListCategory {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: Category[];
+  /** 消息 */
+  message?: string;
+}

+ 19 - 0
src/api/models/resultListConversationVO.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { ConversationVO } from "./conversationVO";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultListConversationVO {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: ConversationVO[];
+  /** 消息 */
+  message?: string;
+}

+ 19 - 0
src/api/models/resultListItemVO.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { ItemVO } from "./itemVO";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultListItemVO {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: ItemVO[];
+  /** 消息 */
+  message?: string;
+}

+ 18 - 0
src/api/models/resultLong.ts

@@ -0,0 +1,18 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 统一响应对象
+ */
+export interface ResultLong {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: number;
+  /** 消息 */
+  message?: string;
+}

+ 19 - 0
src/api/models/resultPageResultItemVO.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { PageResultItemVO } from "./pageResultItemVO";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultPageResultItemVO {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: PageResultItemVO;
+  /** 消息 */
+  message?: string;
+}

+ 19 - 0
src/api/models/resultPageResultMessageVO.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { PageResultMessageVO } from "./pageResultMessageVO";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultPageResultMessageVO {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: PageResultMessageVO;
+  /** 消息 */
+  message?: string;
+}

+ 19 - 0
src/api/models/resultPageResultOrderVO.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { PageResultOrderVO } from "./pageResultOrderVO";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultPageResultOrderVO {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: PageResultOrderVO;
+  /** 消息 */
+  message?: string;
+}

+ 19 - 0
src/api/models/resultPageResultUser.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { PageResultUser } from "./pageResultUser";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultPageResultUser {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: PageResultUser;
+  /** 消息 */
+  message?: string;
+}

+ 19 - 0
src/api/models/resultUserProfileVO.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { UserProfileVO } from "./userProfileVO";
+
+/**
+ * 统一响应对象
+ */
+export interface ResultUserProfileVO {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: UserProfileVO;
+  /** 消息 */
+  message?: string;
+}

+ 18 - 0
src/api/models/resultVoid.ts

@@ -0,0 +1,18 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 统一响应对象
+ */
+export interface ResultVoid {
+  /** 状态码 */
+  code?: number;
+  /** 响应数据 */
+  data?: unknown;
+  /** 消息 */
+  message?: string;
+}

+ 20 - 0
src/api/models/reviewRequest.ts

@@ -0,0 +1,20 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { ReviewRequestResult } from "./reviewRequestResult";
+
+/**
+ * 审核请求
+ */
+export interface ReviewRequest {
+  /** 驳回原因(驳回时必填) */
+  reason?: string;
+  /**
+   * 审核结果
+   * @minLength 1
+   */
+  result: ReviewRequestResult;
+}

+ 19 - 0
src/api/models/reviewRequestResult.ts

@@ -0,0 +1,19 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 审核结果
+ * @minLength 1
+ */
+export type ReviewRequestResult =
+  (typeof ReviewRequestResult)[keyof typeof ReviewRequestResult];
+
+// eslint-disable-next-line @typescript-eslint/no-redeclare
+export const ReviewRequestResult = {
+  APPROVED: "APPROVED",
+  REJECTED: "REJECTED",
+} as const;

+ 12 - 0
src/api/models/searchParams.ts

@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type SearchParams = {
+  keyword: string;
+  pageNum?: number;
+  pageSize?: number;
+};

+ 20 - 0
src/api/models/sendMessageRequest.ts

@@ -0,0 +1,20 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 发送消息请求
+ */
+export interface SendMessageRequest {
+  /** 文本内容 */
+  content?: string;
+  /** 图片URL */
+  imageUrl?: string;
+  /** 关联物品ID */
+  itemId?: number;
+  /** 接收方用户ID */
+  toUserId: number;
+}

+ 11 - 0
src/api/models/soldParams.ts

@@ -0,0 +1,11 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type SoldParams = {
+  pageNum?: number;
+  pageSize?: number;
+};

+ 17 - 0
src/api/models/statusRequest.ts

@@ -0,0 +1,17 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 状态切换请求
+ */
+export interface StatusRequest {
+  /**
+   * 状态值
+   * @minLength 1
+   */
+  status: string;
+}

+ 0 - 24
src/api/models/updateUserAvatarDto.ts

@@ -1,24 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 更新用户头像的实体DTO
- */
-export interface UpdateUserAvatarDto {
-  /**
-   * 头像地址的链接
-   * @minLength 1
-   * @pattern ^(https?)://[^\s/$.?#].[^\s]*$
-   */
-  avatar: string;
-  /**
-   * 用户的ID
-   * @minLength 1
-   */
-  id: string;
-}

+ 0 - 30
src/api/models/updateUserPasswordDto.ts

@@ -1,30 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 用户密码修改使用的DTO
- */
-export interface UpdateUserPasswordDto {
-  /**
-   * 用户ID
-   * @minLength 1
-   */
-  id: string;
-  /**
-   * 用户的新密码
-   * @minLength 4
-   * @maxLength 20
-   */
-  oldPassword: string;
-  /**
-   * 用户的当前密码
-   * @minLength 4
-   * @maxLength 20
-   */
-  password: string;
-}

+ 0 - 25
src/api/models/updateUserStatusDto.ts

@@ -1,25 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 用户状态更新使用的DTO
- */
-export interface UpdateUserStatusDto {
-  /**
-   * 更新后的状态;1:启用,0:禁用
-   * @minLength 1
-   * @minimum 0
-   * @maximum 1
-   */
-  enable: number;
-  /**
-   * 用户ID
-   * @minLength 1
-   */
-  id: string;
-}

+ 25 - 0
src/api/models/user.ts

@@ -0,0 +1,25 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 用户实体
+ */
+export interface User {
+  authStatus?: string;
+  avatarUrl?: string;
+  campusCardUrl?: string;
+  createdAt?: string;
+  id?: number;
+  name?: string;
+  nickname?: string;
+  passwordHash?: string;
+  role?: string;
+  signature?: string;
+  status?: string;
+  studentId?: string;
+  updatedAt?: string;
+}

+ 0 - 39
src/api/models/userDto.ts

@@ -1,39 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 用户新增和修改使用的DTO
- */
-export interface UserDto {
-  /**
-   * 用户账号
-   * @minLength 4
-   * @maxLength 20
-   */
-  account: string;
-  /** 用户ID */
-  id?: string;
-  /**
-   * 用户密码
-   * @minLength 4
-   * @maxLength 20
-   */
-  password: string;
-  /**
-   * 用户角色
-   * @minLength 4
-   * @maxLength 32
-   */
-  role: string;
-  /**
-   * 用户名称
-   * @minLength 2
-   * @maxLength 32
-   */
-  username: string;
-}

+ 18 - 0
src/api/models/userProfileRequest.ts

@@ -0,0 +1,18 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 修改个人资料请求(multipart/form-data)
+ */
+export interface UserProfileRequest {
+  /** 头像文件 */
+  avatar?: Blob;
+  /** 昵称 */
+  nickname?: string;
+  /** 个性签名 */
+  signature?: string;
+}

+ 32 - 0
src/api/models/userProfileVO.ts

@@ -0,0 +1,32 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+/**
+ * 用户信息(不含密码)
+ */
+export interface UserProfileVO {
+  /** 认证状态 */
+  authStatus?: string;
+  /** 头像URL */
+  avatarUrl?: string;
+  /** 创建时间 */
+  createdAt?: string;
+  /** 用户ID */
+  id?: number;
+  /** 姓名 */
+  name?: string;
+  /** 昵称 */
+  nickname?: string;
+  /** 角色 */
+  role?: string;
+  /** 个性签名 */
+  signature?: string;
+  /** 账号状态 */
+  status?: string;
+  /** 学号 */
+  studentId?: string;
+}

+ 0 - 25
src/api/models/userVo.ts

@@ -1,25 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-
-/**
- * 用户响应展示对象
- */
-export interface UserVo {
-  /** 用户账号 */
-  account?: string;
-  /** 用户头像地址 */
-  avatar?: string;
-  /** 用户状态;0:禁用,1:启用 */
-  enable?: number;
-  /** 用户ID */
-  id?: string;
-  /** 用户角色 */
-  role?: string;
-  /** 用户名称 */
-  username?: string;
-}

+ 0 - 117
src/api/user-controller.ts

@@ -1,117 +0,0 @@
-/**
- * Generated by orval v7.0.1 🍺
- * Do not edit manually.
- * Serve API
- * Serve应用接口文档
- * OpenAPI spec version: 0.0.1-SNAPSHOT
- */
-import type {
-  DeleteByIdParams,
-  QueryByPageParams,
-  Response,
-  ResponseBoolean,
-  ResponsePageVoListUserVo,
-  ResponseUserVo,
-  UpdateUserAvatarDto,
-  UpdateUserPasswordDto,
-  UpdateUserStatusDto,
-  UserDto,
-} from "./models";
-import { customAxiosInstance } from "../util/axios-instance";
-
-export const getUserController = () => {
-  const queryByPage = (params: QueryByPageParams) => {
-    return customAxiosInstance<ResponsePageVoListUserVo>({
-      url: `/user`,
-      method: "GET",
-      params,
-    });
-  };
-  const edit = (userDto: UserDto) => {
-    return customAxiosInstance<ResponseBoolean>({
-      url: `/user`,
-      method: "PUT",
-      headers: { "Content-Type": "application/json" },
-      data: userDto,
-    });
-  };
-  const add = (userDto: UserDto) => {
-    return customAxiosInstance<ResponseBoolean>({
-      url: `/user`,
-      method: "POST",
-      headers: { "Content-Type": "application/json" },
-      data: userDto,
-    });
-  };
-  const deleteById = (params: DeleteByIdParams) => {
-    return customAxiosInstance<ResponseBoolean>({
-      url: `/user`,
-      method: "DELETE",
-      params,
-    });
-  };
-  const updateUserStatus = (updateUserStatusDto: UpdateUserStatusDto) => {
-    return customAxiosInstance<Response>({
-      url: `/user/updateStatus`,
-      method: "PUT",
-      headers: { "Content-Type": "application/json" },
-      data: updateUserStatusDto,
-    });
-  };
-  const updatePassword = (updateUserPasswordDto: UpdateUserPasswordDto) => {
-    return customAxiosInstance<Response>({
-      url: `/user/updatePassword`,
-      method: "PUT",
-      headers: { "Content-Type": "application/json" },
-      data: updateUserPasswordDto,
-    });
-  };
-  const updateUserAvatar = (updateUserAvatarDto: UpdateUserAvatarDto) => {
-    return customAxiosInstance<Response>({
-      url: `/user/updateAvatar`,
-      method: "PUT",
-      headers: { "Content-Type": "application/json" },
-      data: updateUserAvatarDto,
-    });
-  };
-  const queryById = (id: string) => {
-    return customAxiosInstance<ResponseUserVo>({
-      url: `/user/${id}`,
-      method: "GET",
-    });
-  };
-  return {
-    queryByPage,
-    edit,
-    add,
-    deleteById,
-    updateUserStatus,
-    updatePassword,
-    updateUserAvatar,
-    queryById,
-  };
-};
-export type QueryByPageResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getUserController>["queryByPage"]>>
->;
-export type EditResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getUserController>["edit"]>>
->;
-export type AddResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getUserController>["add"]>>
->;
-export type DeleteByIdResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getUserController>["deleteById"]>>
->;
-export type UpdateUserStatusResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getUserController>["updateUserStatus"]>>
->;
-export type UpdatePasswordResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getUserController>["updatePassword"]>>
->;
-export type UpdateUserAvatarResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getUserController>["updateUserAvatar"]>>
->;
-export type QueryByIdResult = NonNullable<
-  Awaited<ReturnType<ReturnType<typeof getUserController>["queryById"]>>
->;

+ 36 - 0
src/components/EmptyState.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="empty-state">
+    <el-empty :description="description">
+      <template #image>
+        <el-icon :size="64" class="empty-icon"><Folder /></el-icon>
+      </template>
+      <el-button v-if="actionText" type="primary" @click="$emit('action')">
+        {{ actionText }}
+      </el-button>
+    </el-empty>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Folder } from '@element-plus/icons-vue'
+
+withDefaults(defineProps<{
+  description?: string
+  actionText?: string
+}>(), {
+  description: '暂无数据',
+})
+
+defineEmits<{
+  (e: 'action'): void
+}>()
+</script>
+
+<style scoped>
+.empty-state {
+  padding: 40px 0;
+}
+.empty-icon {
+  color: #c0c4cc;
+}
+</style>

+ 65 - 0
src/components/ImageUploader.vue

@@ -0,0 +1,65 @@
+<template>
+  <el-upload
+    :action="uploadAction"
+    :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>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+
+const props = withDefaults(defineProps<{
+  multiple?: boolean
+  limit?: number
+  modelValue?: string[]
+}>(), {
+  multiple: false,
+  limit: 1,
+  modelValue: () => [],
+})
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string[]): void
+}>()
+
+const uploadAction = '/api/upload'
+const fileList = ref<{ url: string }[]>(props.modelValue.map(url => ({ url })))
+
+watch(() => props.modelValue, (val) => {
+  fileList.value = val.map(url => ({ url }))
+})
+
+const beforeUpload = (file: File) => {
+  if (!file.type.startsWith('image/')) {
+    ElMessage.error('只能上传图片')
+    return false
+  }
+  if (file.size > 10 * 1024 * 1024) {
+    ElMessage.error('图片大小不能超过 10MB')
+    return false
+  }
+  return true
+}
+
+const handleSuccess = (res: { code: number; data?: { url: string } }) => {
+  if (res.code === 200 && res.data?.url) {
+    fileList.value.push({ url: res.data.url })
+    emit('update:modelValue', fileList.value.map(f => f.url))
+  }
+}
+
+const handleRemove = (file: { url: string }) => {
+  fileList.value = fileList.value.filter(f => f.url !== file.url)
+  emit('update:modelValue', fileList.value.map(f => f.url))
+}
+</script>

+ 96 - 96
src/layout/AdminLayout.vue

@@ -1,123 +1,123 @@
-<script setup lang="ts">
-import {useLoginUserStore} from "../store";
-import {computed, type ComputedRef, ref} from "vue";
-import type {RouteRecordRaw} from "vue-router";
-import router, {adminRoutes} from "../router";
-
-const title = ref<string>("标题");
-const loginUserStore = useLoginUserStore()
-const login = computed(()=>loginUserStore.loginUser.isLogin)
-const loginHandler = () => {
-
-}
-const logoutHandler = () => {
-
-}
-const menus:ComputedRef<RouteRecordRaw[] | undefined> = computed(()=>{
-  return adminRoutes[0].children;
-})
-const isCollapse = ref(false)
-const handleOpen = (key: string, keyPath: string[]) => {
-  console.log(key, keyPath)
-}
-const handleClose = (key: string, keyPath: string[]) => {
-  console.log(key, keyPath)
-}
-</script>
-
 <template>
 <template>
-  <el-container>
-    <el-header class="header">
-      <div class="header-left" @click="router.push('/')">
+  <el-container class="admin-container">
+    <el-aside class="sidebar" :width="isCollapse ? '64px' : '200px'">
+      <div class="sidebar-header" @click="router.push('/admin')">
         <img src="/logo.svg" alt="logo" class="logo">
         <img src="/logo.svg" alt="logo" class="logo">
-        <h2 class="title">{{title}}</h2>
-      </div>
-      <h1>后台管理</h1>
-      <div class="header-right">
-        <template v-if="login">
-          <el-dropdown placement="bottom">
-            <el-button> {{ loginUserStore.loginUser?.user?.name }} </el-button>
-            <template #dropdown>
-              <el-dropdown-menu>
-                <el-dropdown-item>个人信息</el-dropdown-item>
-                <el-dropdown-item @click="logoutHandler">退出</el-dropdown-item>
-              </el-dropdown-menu>
-            </template>
-          </el-dropdown>
-        </template>
-        <template v-else>
-          <el-button type="primary" size="small" @click="loginHandler">登录</el-button>
-          <el-button size="small">注册</el-button>
-        </template>
+        <h2 v-show="!isCollapse" class="title">管理后台</h2>
       </div>
       </div>
-    </el-header>
+      <el-menu
+        router
+        :collapse="isCollapse"
+        default-active="/admin/review/user"
+      >
+        <el-menu-item index="/admin/review/user">
+          <el-icon><UserFilled /></el-icon>
+          <template #title>实名审核</template>
+        </el-menu-item>
+        <el-menu-item index="/admin/review/item">
+          <el-icon><Goods /></el-icon>
+          <template #title>物品审核</template>
+        </el-menu-item>
+        <el-menu-item index="/admin/users">
+          <el-icon><Setting /></el-icon>
+          <template #title>用户管理</template>
+        </el-menu-item>
+        <el-menu-item index="/admin/categories">
+          <el-icon><Collection /></el-icon>
+          <template #title>分类管理</template>
+        </el-menu-item>
+      </el-menu>
+    </el-aside>
 
 
-    <el-container class="main-container">
-      <el-aside class="menu">
-        <el-radio-group v-model="isCollapse" style="margin-bottom: 20px;display: flex;justify-content: center">
-          <el-radio-button :value="false">expand</el-radio-button>
-          <el-radio-button :value="true">collapse</el-radio-button>
-        </el-radio-group>
-        <el-menu
-            router
-            class="el-menu-vertical"
-            :collapse="isCollapse"
-            @open="handleOpen"
-            @close="handleClose"
-        >
-          <el-menu-item :index="menu.path" v-for="menu in menus" :key="menu.path">
-            <el-icon><Component :is="menu.meta?.icon"/></el-icon>
-            <template #title>{{menu.meta?.title}}</template>
-          </el-menu-item>
-        </el-menu>
-      </el-aside>
+    <el-container>
+      <el-header class="header">
+        <el-button @click="isCollapse = !isCollapse" text>
+          <el-icon><Fold /></el-icon>
+        </el-button>
+        <div class="header-right">
+          <template v-if="login">
+            <el-dropdown placement="bottom">
+              <el-button text>
+                {{ userStore.state.nickname }}
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item @click="router.push('/home')">返回前台</el-dropdown-item>
+                  <el-dropdown-item divided @click="logoutHandler">退出</el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </template>
+          <template v-else>
+            <el-button type="primary" size="small" @click="router.push('/admin/login')">登录</el-button>
+          </template>
+        </div>
+      </el-header>
       <el-main>
       <el-main>
-        <router-view/>
+        <router-view />
       </el-main>
       </el-main>
     </el-container>
     </el-container>
   </el-container>
   </el-container>
-
 </template>
 </template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { useUserStore } from '../store'
+import router from '../router'
+import {
+  UserFilled,
+  Goods,
+  Setting,
+  Collection,
+  Fold,
+} from '@element-plus/icons-vue'
+
+const userStore = useUserStore()
+const login = computed(() => userStore.state.isLoggedIn)
+const isCollapse = ref(false)
+
+const logoutHandler = () => {
+  userStore.logout()
+  router.push('/admin/login')
+}
+</script>
+
 <style scoped>
 <style scoped>
-.el-container {
+.admin-container {
   height: 100vh;
   height: 100vh;
 }
 }
-.header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 20px;
-  margin-bottom: 10px;
-  border-bottom: 1px solid
+.sidebar {
+  background: #304156;
+  transition: width 0.3s;
 }
 }
-.header-left {
+.sidebar-header {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   gap: 10px;
   gap: 10px;
+  padding: 16px;
+  cursor: pointer;
+  color: #fff;
 }
 }
 .logo {
 .logo {
-  width: 40px;
-  height: 40px;
+  width: 32px;
+  height: 32px;
 }
 }
 .title {
 .title {
   margin: 0;
   margin: 0;
-  font-size: 20px;
+  font-size: 18px;
+  white-space: nowrap;
+}
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 20px;
+  background: #fff;
+  border-bottom: 1px solid #e4e7ed;
 }
 }
-
 .header-right {
 .header-right {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   gap: 10px;
   gap: 10px;
 }
 }
-
-.menu{
-  width: 200px;
-}
-.main-container {
-  flex: 1;
-}
-.el-menu-vertical-demo:not(.el-menu--collapse) {
-  width: 200px;
-  min-height: 400px;
-}
-</style>
+</style>

+ 81 - 42
src/layout/UserLayout.vue

@@ -2,23 +2,20 @@
   <div class="user-layout">
   <div class="user-layout">
     <el-container>
     <el-container>
       <el-header class="header">
       <el-header class="header">
-        <div class="header-left" @click="router.push('/')">
+        <div class="header-left" @click="router.push('/home')">
           <img :src="metaStore.logo" alt="logo" class="logo">
           <img :src="metaStore.logo" alt="logo" class="logo">
-          <h2 class="title">{{metaStore.title}}</h2>
+          <h2 class="title">{{ metaStore.title }}</h2>
         </div>
         </div>
         <el-menu
         <el-menu
-            :default-active="activeIndex"
-            mode="horizontal"
-            router
-            @select="handleSelect"
-            class="nav-menu"
+          :default-active="activeIndex"
+          mode="horizontal"
+          router
+          class="nav-menu"
         >
         >
-          <el-menu-item :index="menu.path" v-for="menu in menus" :key="menu.path">
-            <el-icon><component :is="menu.meta?.icon"/></el-icon>
-            <template #title>{{ menu.meta?.title }}</template>
+          <el-menu-item index="/home">
+            <template #title>首页</template>
           </el-menu-item>
           </el-menu-item>
           <el-menu-item v-if="isAdmin" index="/admin">
           <el-menu-item v-if="isAdmin" index="/admin">
-            <el-icon><Tools/></el-icon>
             <template #title>后台管理</template>
             <template #title>后台管理</template>
           </el-menu-item>
           </el-menu-item>
         </el-menu>
         </el-menu>
@@ -27,66 +24,82 @@
             <el-dropdown placement="bottom">
             <el-dropdown placement="bottom">
               <el-button class="user-btn" text>
               <el-button class="user-btn" text>
                 <el-icon class="user-avatar"><UserFilled /></el-icon>
                 <el-icon class="user-avatar"><UserFilled /></el-icon>
-                <span class="username">{{ loginUserStore.loginUser?.user?.name }}</span>
+                <span class="username">{{ userStore.state.nickname }}</span>
               </el-button>
               </el-button>
               <template #dropdown>
               <template #dropdown>
                 <el-dropdown-menu>
                 <el-dropdown-menu>
-                  <el-dropdown-item>个人信息</el-dropdown-item>
+                  <el-dropdown-item @click="router.push('/profile')">个人信息</el-dropdown-item>
                   <el-dropdown-item divided @click="logoutHandler">退出登录</el-dropdown-item>
                   <el-dropdown-item divided @click="logoutHandler">退出登录</el-dropdown-item>
                 </el-dropdown-menu>
                 </el-dropdown-menu>
               </template>
               </template>
             </el-dropdown>
             </el-dropdown>
           </template>
           </template>
           <template v-else>
           <template v-else>
-            <el-button type="primary" size="small" @click="loginHandler">登录</el-button>
-            <el-button size="small" @click="router.push('/login')">注册</el-button>
+            <el-button type="primary" size="small" @click="router.push('/login')">登录</el-button>
+            <el-button size="small" @click="router.push('/register')">注册</el-button>
           </template>
           </template>
         </div>
         </div>
       </el-header>
       </el-header>
 
 
-      <el-main>
-        <router-view/>
+      <el-main class="main">
+        <router-view />
       </el-main>
       </el-main>
 
 
-      <el-footer>Footer</el-footer>
+      <el-footer class="mobile-footer">
+        <el-row class="tab-bar">
+          <el-col :span="6" @click="router.push('/home')">
+            <el-icon><HomeFilled /></el-icon>
+            <span>首页</span>
+          </el-col>
+          <el-col :span="6" @click="router.push('/publish')">
+            <el-icon><Plus /></el-icon>
+            <span>发布</span>
+          </el-col>
+          <el-col :span="6" @click="router.push('/messages')">
+            <el-icon><ChatDotSquare /></el-icon>
+            <span>消息</span>
+          </el-col>
+          <el-col :span="6" @click="router.push('/profile')">
+            <el-icon><User /></el-icon>
+            <span>我的</span>
+          </el-col>
+        </el-row>
+      </el-footer>
     </el-container>
     </el-container>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import {computed, ref} from 'vue'
-import {useLoginUserStore, useMetaStore} from "../store";
-import router, {userRoutes} from "../router";
+import { computed, ref } from 'vue'
+import { useUserStore, useMetaStore } from '../store'
+import router from '../router'
 import {
 import {
-  Tools,
-  UserFilled
+  HomeFilled,
+  Plus,
+  ChatDotSquare,
+  User,
+  UserFilled,
 } from '@element-plus/icons-vue'
 } from '@element-plus/icons-vue'
+
 const metaStore = useMetaStore()
 const metaStore = useMetaStore()
-const loginUserStore = useLoginUserStore()
-const login = computed(() => loginUserStore.loginUser.isLogin)
-const isAdmin = computed(() => loginUserStore.loginUser.user?.role === 'admin')
-const loginHandler = () => {
-  router.push('/login')
-}
-let activeIndex = ref('/')
-const menus = computed(()=>{
-  return userRoutes[0].children;
-})
-const handleSelect = () => {
-  //todo: 处理菜单选择
-}
+const userStore = useUserStore()
+const login = computed(() => userStore.state.isLoggedIn)
+const isAdmin = computed(() => userStore.state.role === 'ADMIN')
+const activeIndex = ref('/home')
+
 const logoutHandler = () => {
 const logoutHandler = () => {
-  loginUserStore.logoutUser()
-  router.push('/')
+  userStore.logout()
+  router.push('/home')
 }
 }
 </script>
 </script>
+
 <style scoped>
 <style scoped>
 .user-layout {
 .user-layout {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   min-height: 100vh;
   min-height: 100vh;
 }
 }
-el-main{
+.main {
   flex: 1;
   flex: 1;
 }
 }
 .header {
 .header {
@@ -99,6 +112,7 @@ el-main{
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   gap: 10px;
   gap: 10px;
+  cursor: pointer;
 }
 }
 .logo {
 .logo {
   width: 40px;
   width: 40px;
@@ -123,7 +137,6 @@ el-main{
   font-size: 14px;
   font-size: 14px;
   color: #606266;
   color: #606266;
 }
 }
-
 .user-btn {
 .user-btn {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -131,13 +144,39 @@ el-main{
   cursor: pointer;
   cursor: pointer;
   transition: color 0.2s;
   transition: color 0.2s;
 }
 }
-
 .user-btn:hover {
 .user-btn:hover {
   color: #db2777;
   color: #db2777;
 }
 }
-
 .user-avatar {
 .user-avatar {
   font-size: 20px;
   font-size: 20px;
   color: #be185d;
   color: #be185d;
 }
 }
+.mobile-footer {
+  display: none;
+  padding: 0;
+}
+.tab-bar {
+  text-align: center;
+  font-size: 12px;
+  padding: 8px 0;
+  border-top: 1px solid #e4e7ed;
+  background: #fff;
+}
+.tab-bar .el-col {
+  cursor: pointer;
+}
+.tab-bar .el-col .el-icon {
+  display: block;
+  font-size: 20px;
+  margin-bottom: 2px;
+}
+
+@media (max-width: 768px) {
+  .nav-menu {
+    display: none;
+  }
+  .mobile-footer {
+    display: block;
+  }
+}
 </style>
 </style>

+ 150 - 95
src/router/index.ts

@@ -1,106 +1,161 @@
-import {createRouter, createWebHistory, type Router, type RouteRecordRaw} from 'vue-router'
+import { createRouter, createWebHistory, type Router, type RouteRecordRaw } from 'vue-router'
+import { useUserStore } from '../store'
+import { ElMessage } from 'element-plus'
 
 
-import HomeView from '../view/HomeView.vue'
-import AboutView from '../view/AboutView.vue'
-import LoginView from '../view/LoginView.vue'
 import UserLayout from '../layout/UserLayout.vue'
 import UserLayout from '../layout/UserLayout.vue'
-import AdminLayout from "../layout/AdminLayout.vue";
-import UserView from "../view/UserView.vue";
-import {
-    HomeFilled,
-    Menu,
-    User,
-} from '@element-plus/icons-vue'
-import {useLoginUserStore} from "../store";
-import {ElMessage} from "element-plus";
+import AdminLayout from '../layout/AdminLayout.vue'
 
 
-export const userRoutes:RouteRecordRaw[] = [
-    {
-        path: '/',
-        name: 'commonLayout',
-        component: UserLayout,
-        children: [
-            {
-                path: '/',
-                name: 'home',
-                component: HomeView,
-                meta: {
-                    title: '首页',
-                    icon: HomeFilled
-                }
-            },
-            {
-                path: '/about',
-                name: 'about',
-                component: AboutView,
-                meta: {
-                    title: '关于',
-                    icon: Menu
-                }
-            },
-        ],
-    },
-]
-export const adminRoutes:RouteRecordRaw[] = [
-    {
-        path: '/admin',
-        name: 'adminLayout',
-        component: AdminLayout,
-        children:[
-            {
-                path: '/admin/users',
-                name: 'users',
-                component: UserView,
-                meta:{
-                    title: '用户管理',
-                    icon: User,
-                    requiresAdmin: true
-                }
-            },
-            {
-                path:'/admin/other',
-                name:'other',
-                component: AdminLayout,
-                meta:{
-                    title: '其他',
-                    icon: Menu,
-                    requiresAdmin: true
-                }
-            }
-        ],
-        meta: {
-            requiresAdmin: true
-        }
-    }
-]
+const routes: RouteRecordRaw[] = [
+  // 独立页面 — 无布局
+  {
+    path: '/login',
+    name: 'login',
+    component: () => import('../view/Login.vue'),
+    meta: { noAuth: true },
+  },
+  {
+    path: '/register',
+    name: 'register',
+    component: () => import('../view/Register.vue'),
+    meta: { noAuth: true },
+  },
+  {
+    path: '/admin/login',
+    name: 'adminLogin',
+    component: () => import('../view/admin/Login.vue'),
+    meta: { noAuth: true },
+  },
 
 
-const router : Router = createRouter({
-    history: createWebHistory(),
-    routes: [
-        {
-            path: '/login',
-            name: 'login',
-            component: LoginView
-        },
-        ...userRoutes,
-        ...adminRoutes
+  // C端 — UserLayout
+  {
+    path: '/',
+    component: UserLayout,
+    children: [
+      { 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'),
+      },
+      {
+        path: 'publish',
+        name: 'publish',
+        component: () => import('../view/Publish.vue'),
+        meta: { requiresAuth: true, requiresVerified: 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端 — AdminLayout
+  {
+    path: '/admin',
+    component: AdminLayout,
+    meta: { requiresAuth: true, requiresAdmin: true },
+    children: [
+      { path: '', redirect: '/admin/review/user' },
+      {
+        path: 'review/user',
+        name: 'reviewUser',
+        component: () => import('../view/admin/ReviewUser.vue'),
+      },
+      {
+        path: 'review/item',
+        name: 'reviewItem',
+        component: () => import('../view/admin/ReviewItem.vue'),
+      },
+      {
+        path: 'users',
+        name: 'userManage',
+        component: () => import('../view/admin/UserManage.vue'),
+      },
+      {
+        path: 'categories',
+        name: 'categoryManage',
+        component: () => import('../view/admin/CategoryManage.vue'),
+      },
+    ],
+  },
+]
+
+const router: Router = createRouter({
+  history: createWebHistory(),
+  routes,
+})
+
 router.beforeEach((to, _from, next) => {
 router.beforeEach((to, _from, next) => {
-    if (to.matched.some(record => record.meta.requiresAdmin)) {
-        const loginUserStore = useLoginUserStore()
-        if (!loginUserStore.loginUser.isLogin) {
-            ElMessage.warning('请先登录')
-            next({path: '/login', query: {redirect: to.fullPath}})
-        } else if (loginUserStore.loginUser.user?.role !== 'admin') {
-            ElMessage.error('无权访问')
-            next('/')
-        } else {
-            next()
-        }
+  const userStore = useUserStore()
+
+  if (to.meta.requiresAdmin && !to.meta.noAuth) {
+    if (!userStore.state.isLoggedIn) {
+      ElMessage.warning('请先登录')
+      next({ path: '/login', query: { redirect: to.fullPath } })
+    } else if (userStore.state.role !== 'ADMIN') {
+      ElMessage.error('无权访问')
+      next('/')
+    } else {
+      next()
+    }
+  } else if (to.meta.requiresAuth && !to.meta.noAuth) {
+    if (!userStore.state.isLoggedIn) {
+      next({ path: '/login', query: { redirect: to.fullPath } })
     } else {
     } else {
-        next()
+      next()
     }
     }
+  } else {
+    next()
+  }
 })
 })
 
 
 export default router
 export default router

+ 2 - 2
src/store/index.ts

@@ -1,2 +1,2 @@
-export  {useMetaStore} from "./meta.ts";
-export  {useLoginUserStore} from "./user.ts";
+export { useMetaStore } from "./meta.ts";
+export { useUserStore } from "./user.ts";

+ 53 - 48
src/store/user.ts

@@ -1,49 +1,54 @@
-import {defineStore} from "pinia";
-import type {LoginUser, User} from "../type";
-import {computed, reactive} from "vue";
+import { defineStore } from 'pinia'
+import { computed, reactive } from 'vue'
 
 
-export const useLoginUserStore = defineStore('loginUser', ()=>{
-    //state
-    const loginUser = reactive<LoginUser>({
-        isLogin: false,
-        user:{
-            id:"",
-            name:"未登录",
-            role:"guest"
-        }
-    })
-    //getters
-    const userId = computed(()=>{
-        return loginUser.user?.id
-    })
-    //actions
-    function updateUser(user:User):void {
-        loginUser.user = {...user}
-    }
-    function loginSuccess(user:User):void {
-        updateUser(user)
-        loginUser.isLogin = true;
-    }
-    function logoutUser():void {
-        loginUser.isLogin = false
-        loginUser.user = {
-            id: '',
-            name: '未登录',
-            role: 'guest'
-        }
-        localStorage.removeItem('token')
-    }
-   return{
-        //States
-       loginUser,
-       //Getters
-       userId,
-       //Actions
-       updateUser,
-       loginSuccess,
-       logoutUser,
-   }
-},{
-    // ✅ 启用持久化
-    persist: true
-})
+export const useUserStore = defineStore('user', () => {
+  const state = reactive({
+    userId: null as number | null,
+    nickname: '',
+    avatarUrl: '',
+    role: '',
+    authStatus: '',
+    isLoggedIn: false,
+  })
+
+  const isAuthenticated = computed(() => state.authStatus === 'AUTHENTICATED')
+  const isAdmin = computed(() => state.role === 'ADMIN')
+
+  function login(user: {
+    id: number
+    nickname: string
+    avatarUrl?: string
+    role: string
+    authStatus: string
+  }) {
+    state.userId = user.id
+    state.nickname = user.nickname
+    state.avatarUrl = user.avatarUrl ?? ''
+    state.role = user.role
+    state.authStatus = user.authStatus
+    state.isLoggedIn = true
+  }
+
+  function logout() {
+    state.userId = null
+    state.nickname = ''
+    state.avatarUrl = ''
+    state.role = ''
+    state.authStatus = ''
+    state.isLoggedIn = false
+  }
+
+  function updateProfile(profile: { nickname?: string; avatarUrl?: string }) {
+    if (profile.nickname !== undefined) state.nickname = profile.nickname
+    if (profile.avatarUrl !== undefined) state.avatarUrl = profile.avatarUrl
+  }
+
+  return {
+    state,
+    isAuthenticated,
+    isAdmin,
+    login,
+    logout,
+    updateProfile,
+  }
+})

+ 19 - 72
src/style.css

@@ -1,79 +1,26 @@
-:root {
-  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
-  line-height: 1.5;
-  font-weight: 400;
-
-  color-scheme: light dark;
-  color: rgba(255, 255, 255, 0.87);
-  background-color: #242424;
-
-  font-synthesis: none;
-  text-rendering: optimizeLegibility;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-a {
-  font-weight: 500;
-  color: #646cff;
-  text-decoration: inherit;
-}
-a:hover {
-  color: #535bf2;
-}
-
-body {
+* {
   margin: 0;
   margin: 0;
-  display: flex;
-  place-items: center;
-  min-width: 320px;
-  min-height: 100vh;
-}
-
-h1 {
-  font-size: 3.2em;
-  line-height: 1.1;
-}
-
-button {
-  border-radius: 8px;
-  border: 1px solid transparent;
-  padding: 0.6em 1.2em;
-  font-size: 1em;
-  font-weight: 500;
-  font-family: inherit;
-  background-color: #1a1a1a;
-  cursor: pointer;
-  transition: border-color 0.25s;
-}
-button:hover {
-  border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
-  outline: 4px auto -webkit-focus-ring-color;
-}
-
-.card {
-  padding: 2em;
+  padding: 0;
+  box-sizing: border-box;
 }
 }
 
 
+html,
+body,
 #app {
 #app {
-  max-width: 1280px;
-  margin: 0 auto;
-  padding: 2rem;
-  text-align: center;
+  height: 100%;
+  font-family:
+    system-ui,
+    -apple-system,
+    'Segoe UI',
+    Roboto,
+    Helvetica,
+    Arial,
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
 }
 }
 
 
-@media (prefers-color-scheme: light) {
-  :root {
-    color: #213547;
-    background-color: #ffffff;
-  }
-  a:hover {
-    color: #747bff;
-  }
-  button {
-    background-color: #f9f9f9;
-  }
+a {
+  color: inherit;
+  text-decoration: none;
 }
 }

+ 0 - 11
src/type/index.ts

@@ -1,11 +0,0 @@
-export interface User{
-    id:string,
-    name:string,
-    avatar?: string
-    role: 'admin' | 'user' | 'guest'
-}
-export interface LoginUser{
-    isLogin:boolean,
-    token?:string
-    user?:User
-}

+ 29 - 48
src/util/axios-instance.ts

@@ -1,62 +1,43 @@
-import axios, {type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
+import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
+import router from '@/router'
 
 
-// 自定义配置接口
 export interface CustomInstanceConfig extends AxiosRequestConfig {
 export interface CustomInstanceConfig extends AxiosRequestConfig {
-    // 可以添加自定义配置
-    skipAuth?: boolean; // 是否跳过鉴权
+  skipAuth?: boolean
 }
 }
 
 
-// 创建 Axios 实例
 const axiosInstance: AxiosInstance = axios.create({
 const axiosInstance: AxiosInstance = axios.create({
-    baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
-    timeout: 30000,
-    headers: {
-        'Content-Type': 'application/json',
-    },
-});
+  baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
+  timeout: 30000,
+  withCredentials: true,
+  headers: {
+    'Content-Type': 'application/json',
+  },
+})
 
 
-// 请求拦截器
-axiosInstance.interceptors.request.use(
-    (config) => {
-        // 添加 token
-        const token = localStorage.getItem('token');
-        if (token) {
-            config.headers.Authorization = `Bearer ${token}`;
-        }
-        return config;
-    },
-    (error) => {
-        return Promise.reject(error);
-    }
-);
+axiosInstance.interceptors.request.use((config) => config, (error) => Promise.reject(error))
 
 
-// 响应拦截器
 axiosInstance.interceptors.response.use(
 axiosInstance.interceptors.response.use(
-    (response: AxiosResponse) => {
-        // 根据后端返回格式调整
-        if (response.data.code !== 0 && response.data.code !== 200) {
-            return Promise.reject(new Error(response.data.message || '请求失败'));
-        }
-        return response.data;
-    },
-    (error) => {
-        // 统一错误处理
-        if (error.response?.status === 401) {
-            // 未授权,跳转登录
-            window.location.href = '/login';
-        }
-        return Promise.reject(error);
+  (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) {
+      router.push('/login')
     }
     }
-);
+    return Promise.reject(error)
+  },
+)
 
 
-// 自定义实例函数(供 Orval 使用)
 export const customAxiosInstance = <T>(config: CustomInstanceConfig): Promise<T> => {
 export const customAxiosInstance = <T>(config: CustomInstanceConfig): Promise<T> => {
-    return axiosInstance(config);
-};
+  return axiosInstance(config)
+}
 
 
-// 也可以导出第二参数形式(如需处理完整响应)
 export const customAxiosInstanceWithFullResponse = <T>(
 export const customAxiosInstanceWithFullResponse = <T>(
-    config: CustomInstanceConfig
+  config: CustomInstanceConfig,
 ): Promise<AxiosResponse<T>> => {
 ): Promise<AxiosResponse<T>> => {
-    return axiosInstance(config);
-};
+  return axiosInstance(config)
+}

+ 0 - 14
src/view/AboutView.vue

@@ -1,14 +0,0 @@
-<template>
-<h1>这是about页面</h1>
-  <el-text>
-    这是基于Vite + Vue + TypeScript构建的前端模板
-  </el-text>
-</template>
-
-<script setup lang="ts">
-console.log('about')
-</script>
-
-<style scoped>
-
-</style>

+ 108 - 0
src/view/ChangePassword.vue

@@ -0,0 +1,108 @@
+<template>
+  <div class="change-password">
+    <h1 class="page-title">修改密码</h1>
+
+    <el-card class="password-card">
+      <el-form
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="100px"
+        @submit.prevent="handleSubmit"
+      >
+        <el-form-item label="旧密码" prop="oldPassword">
+          <el-input v-model="form.oldPassword" type="password" show-password />
+        </el-form-item>
+
+        <el-form-item label="新密码" prop="newPassword">
+          <el-input v-model="form.newPassword" type="password" show-password />
+        </el-form-item>
+
+        <el-form-item label="确认密码" prop="confirmPassword">
+          <el-input v-model="form.confirmPassword" type="password" show-password />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" :loading="submitting" @click="handleSubmit">提交</el-button>
+          <el-button @click="router.back()">取消</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { getAuth } from '../api/endpoints/auth'
+
+const router = useRouter()
+const formRef = ref<FormInstance>()
+const submitting = ref(false)
+
+const form = reactive({
+  oldPassword: '',
+  newPassword: '',
+  confirmPassword: '',
+})
+
+const validateConfirm = (_rule: unknown, value: string, callback: (err?: Error) => void) => {
+  if (value !== form.newPassword) {
+    callback(new Error('两次输入的密码不一致'))
+  } else {
+    callback()
+  }
+}
+
+const rules: FormRules = {
+  oldPassword: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
+  newPassword: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 4, message: '密码长度至少4位', trigger: 'blur' },
+  ],
+  confirmPassword: [
+    { required: true, message: '请确认新密码', trigger: 'blur' },
+    { validator: validateConfirm, trigger: 'blur' },
+  ],
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+
+  submitting.value = true
+  try {
+    const res = await getAuth().changePassword({
+      oldPassword: form.oldPassword,
+      newPassword: form.newPassword,
+    })
+    if (res.code === 200) {
+      ElMessage.success('密码修改成功')
+      router.push('/profile')
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  } finally {
+    submitting.value = false
+  }
+}
+</script>
+
+<style scoped>
+.change-password {
+  max-width: 500px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.page-title {
+  font-size: 24px;
+  margin-bottom: 20px;
+}
+.password-card {
+  padding: 20px;
+}
+</style>

+ 182 - 0
src/view/Chat.vue

@@ -0,0 +1,182 @@
+<template>
+  <div class="chat-page">
+    <div class="chat-header">
+      <el-button text @click="router.back()">
+        <el-icon><ArrowLeft /></el-icon>
+      </el-button>
+      <span class="chat-partner">{{ partnerName }}</span>
+    </div>
+
+    <div class="message-list" ref="messageListRef">
+      <div
+        v-for="msg in messages"
+        :key="msg.id"
+        :class="['message-row', msg.fromUserId === currentUserId ? 'own' : 'other']"
+      >
+        <el-avatar :src="msg.fromUserAvatar" :size="32" />
+        <div class="message-bubble">
+          <p>{{ msg.content }}</p>
+          <el-image v-if="msg.imageUrl" :src="msg.imageUrl" fit="cover" class="msg-image" />
+        </div>
+      </div>
+      <div v-if="loading" class="loading-more">
+        <el-icon class="is-loading"><Loading /></el-icon>
+      </div>
+    </div>
+
+    <div class="input-area">
+      <el-input
+        v-model="text"
+        type="textarea"
+        :rows="2"
+        placeholder="输入消息..."
+        @keyup.enter.ctrl="handleSend"
+      />
+      <el-button type="primary" @click="handleSend">
+        发送
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, nextTick } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { ArrowLeft, Loading } from '@element-plus/icons-vue'
+import { getMessages } from '../api/endpoints/messages'
+import { useUserStore } from '../store'
+import type { MessageVO } from '../api/models'
+
+const route = useRoute()
+const router = useRouter()
+const userStore = useUserStore()
+const messageListRef = ref<HTMLElement | null>(null)
+const messages = ref<MessageVO[]>([])
+const text = ref('')
+const loading = ref(false)
+const partnerName = ref('')
+
+const currentUserId = userStore.state.userId
+const targetUserId = Number(route.params.userId)
+
+const scrollToBottom = async () => {
+  await nextTick()
+  if (messageListRef.value) {
+    messageListRef.value.scrollTop = messageListRef.value.scrollHeight
+  }
+}
+
+const loadMessages = async () => {
+  loading.value = true
+  try {
+    const res = await getMessages().history(targetUserId)
+    if (res.code === 200) {
+      messages.value = (res.data?.list ?? []).reverse()
+      if (messages.value.length > 0) {
+        partnerName.value = messages.value[0].fromUserId === currentUserId
+          ? (messages.value[0].toUserNickname ?? '')
+          : (messages.value[0].fromUserNickname ?? '')
+      }
+      await scrollToBottom()
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleSend = async () => {
+  if (!text.value.trim()) return
+  try {
+    const res = await getMessages().send({ toUserId: targetUserId, content: text.value.trim() })
+    if (res.code === 200) {
+      messages.value.push({
+        id: res.data,
+        fromUserId: currentUserId,
+        content: text.value.trim(),
+        createdAt: new Date().toISOString(),
+      } as MessageVO)
+      text.value = ''
+      await scrollToBottom()
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  }
+}
+
+onMounted(loadMessages)
+</script>
+
+<style scoped>
+.chat-page {
+  display: flex;
+  flex-direction: column;
+  height: calc(100vh - 120px);
+  max-width: 700px;
+  margin: 0 auto;
+}
+.chat-header {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 16px;
+  border-bottom: 1px solid #e4e7ed;
+  background: #fff;
+}
+.chat-partner {
+  font-size: 16px;
+  font-weight: 500;
+}
+.message-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+}
+.message-row {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 16px;
+}
+.message-row.own {
+  flex-direction: row-reverse;
+}
+.message-bubble {
+  max-width: 70%;
+  padding: 10px 14px;
+  border-radius: 12px;
+  font-size: 14px;
+  line-height: 1.5;
+}
+.message-row.other .message-bubble {
+  background: #f0f0f0;
+  border-bottom-left-radius: 4px;
+}
+.message-row.own .message-bubble {
+  background: #409eff;
+  color: #fff;
+  border-bottom-right-radius: 4px;
+}
+.msg-image {
+  width: 120px;
+  height: 120px;
+  border-radius: 4px;
+  margin-top: 8px;
+  cursor: pointer;
+}
+.loading-more {
+  text-align: center;
+  padding: 8px;
+}
+.input-area {
+  display: flex;
+  gap: 8px;
+  padding: 12px 16px;
+  border-top: 1px solid #e4e7ed;
+  background: #fff;
+}
+.input-area .el-button {
+  align-self: flex-end;
+}
+</style>

+ 212 - 0
src/view/Home.vue

@@ -0,0 +1,212 @@
+<template>
+  <div class="home">
+    <div class="search-bar">
+      <el-input
+        v-model="keyword"
+        placeholder="搜索闲置物品"
+        :prefix-icon="Search"
+        size="large"
+        clearable
+        @keyup.enter="onSearch"
+      />
+    </div>
+
+    <el-tabs v-model="activeCategory" class="category-tabs" @tab-change="onCategoryChange">
+      <el-tab-pane label="全部" name="" />
+      <el-tab-pane v-for="cat in categories" :key="cat.id" :label="cat.name" :name="String(cat.id)" />
+    </el-tabs>
+
+    <el-row :gutter="12" class="item-grid">
+      <el-col
+        v-for="item in itemList"
+        :key="item.id"
+        :xs="24"
+        :sm="12"
+        :md="8"
+        :lg="6"
+        class="item-col"
+      >
+        <el-card :body-style="{ padding: '0px' }" shadow="hover" class="item-card" @click="goDetail(item.id)">
+          <el-image
+            :src="item.images?.[0]"
+            fit="cover"
+            class="item-image"
+          >
+            <template #error>
+              <div class="image-placeholder">
+                <el-icon :size="32"><Picture /></el-icon>
+              </div>
+            </template>
+          </el-image>
+          <div class="item-info">
+            <h4 class="item-title">{{ item.title }}</h4>
+            <div class="item-meta">
+              <span class="price">¥{{ item.price }}</span>
+              <span class="seller">{{ item.sellerNickname }}</span>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <div v-if="loading" class="loading-skeleton">
+      <el-skeleton :rows="3" animated />
+    </div>
+
+    <div v-if="!loading && itemList.length === 0" class="empty-state">
+      <el-empty description="暂无物品" />
+    </div>
+
+    <el-pagination
+      v-if="total > pageSize"
+      v-model:current-page="pageNum"
+      :total="total"
+      :page-size="pageSize"
+      layout="prev, pager, next"
+      class="pagination"
+      @current-change="loadItems"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { Search, Picture } from '@element-plus/icons-vue'
+import { getItems } from '../api/endpoints/items'
+import { getCategories } from '../api/endpoints/categories'
+import type { ItemVO, Category } from '../api/models'
+
+const router = useRouter()
+const keyword = ref('')
+const activeCategory = ref('')
+const itemList = ref<ItemVO[]>([])
+const categories = ref<Category[]>([])
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = 10
+const loading = ref(false)
+
+const loadItems = async () => {
+  loading.value = true
+  try {
+    const categoryId = activeCategory.value ? Number(activeCategory.value) : undefined
+    const api = getItems()
+    const params = { pageNum: pageNum.value, pageSize }
+    const res = categoryId
+      ? await api.byCategory(categoryId, params)
+      : await api.list(params)
+    if (res.code === 200) {
+      itemList.value = res.data?.list ?? []
+      total.value = res.data?.total ?? 0
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+const onSearch = () => {
+  if (keyword.value.trim()) {
+    router.push({ path: '/search', query: { keyword: keyword.value.trim() } })
+  }
+}
+
+const onCategoryChange = () => {
+  pageNum.value = 1
+  loadItems()
+}
+
+const goDetail = (id?: number) => {
+  if (id) router.push(`/item/${id}`)
+}
+
+onMounted(async () => {
+  await loadItems()
+  try {
+    const res = await getCategories().list1()
+    if (res.code === 200) {
+      categories.value = res.data ?? []
+    }
+  } catch {
+    // categories optional
+  }
+})
+</script>
+
+<style scoped>
+.home {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.search-bar {
+  margin-bottom: 20px;
+}
+.category-tabs {
+  margin-bottom: 20px;
+}
+.item-grid {
+  margin-bottom: 20px;
+}
+.item-col {
+  margin-bottom: 12px;
+}
+.item-card {
+  cursor: pointer;
+  border-radius: 8px;
+  overflow: hidden;
+  transition: transform 0.2s;
+}
+.item-card:hover {
+  transform: translateY(-2px);
+}
+.item-image {
+  width: 100%;
+  height: 180px;
+  display: block;
+}
+.image-placeholder {
+  height: 180px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f5f7fa;
+  color: #c0c4cc;
+}
+.item-info {
+  padding: 12px;
+}
+.item-title {
+  margin: 0 0 8px;
+  font-size: 14px;
+  font-weight: 500;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.item-meta {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.price {
+  color: #f56c6c;
+  font-size: 16px;
+  font-weight: 600;
+}
+.seller {
+  font-size: 12px;
+  color: #909399;
+}
+.loading-skeleton {
+  padding: 20px;
+}
+.empty-state {
+  padding: 40px 0;
+}
+.pagination {
+  display: flex;
+  justify-content: center;
+  padding: 20px 0;
+}
+</style>

+ 0 - 12
src/view/HomeView.vue

@@ -1,12 +0,0 @@
-<template>
-<el-text>
-  这是基于Vite + Vue + TypeScript构建的前端模板
-</el-text>
-</template>
-
-<script setup lang="ts">
-</script>
-
-<style scoped>
-
-</style>

+ 163 - 0
src/view/ItemDetail.vue

@@ -0,0 +1,163 @@
+<template>
+  <div class="item-detail">
+    <div v-loading="loading">
+      <template v-if="item">
+        <el-carousel class="image-carousel" height="400px" indicator-position="outside">
+          <el-carousel-item v-for="(img, i) in item.images" :key="i">
+            <el-image :src="img" fit="contain" class="detail-image" />
+          </el-carousel-item>
+        </el-carousel>
+
+        <div class="detail-info">
+          <h1 class="detail-title">{{ item.title }}</h1>
+          <div class="detail-price">¥{{ item.price }}</div>
+          <div class="detail-meta">
+            <el-tag>{{ item.tradeMethod === 'PICKUP' ? '自提' : '送货' }}</el-tag>
+            <el-tag v-if="item.categoryName" type="info">{{ item.categoryName }}</el-tag>
+          </div>
+          <p class="detail-desc">{{ item.description }}</p>
+
+          <div class="seller-info">
+            <el-avatar :src="item.sellerAvatar" />
+            <span class="seller-name">{{ item.sellerNickname }}</span>
+          </div>
+
+          <div class="action-bar">
+            <el-button type="primary" size="large" @click="handleContact">
+              <el-icon><ChatDotSquare /></el-icon>联系卖家
+            </el-button>
+            <el-button type="danger" size="large" @click="handleBuy">
+              <el-icon><ShoppingCart /></el-icon>我想要
+            </el-button>
+          </div>
+        </div>
+      </template>
+      <el-empty v-else-if="!loading" description="物品不存在" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { ChatDotSquare, ShoppingCart } from '@element-plus/icons-vue'
+import { getItems } from '../api/endpoints/items'
+import { getOrders } from '../api/endpoints/orders'
+import { useUserStore } from '../store'
+import type { ItemVO } from '../api/models'
+
+const route = useRoute()
+const router = useRouter()
+const userStore = useUserStore()
+const item = ref<ItemVO | null>(null)
+const loading = ref(false)
+
+const loadItem = async () => {
+  loading.value = true
+  try {
+    const id = Number(route.params.id)
+    const res = await getItems().detail(id)
+    if (res.code === 200) {
+      item.value = res.data!
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleContact = () => {
+  if (!userStore.state.isLoggedIn) {
+    ElMessage.warning('请先登录')
+    router.push('/login')
+    return
+  }
+  if (item.value?.sellerId) {
+    router.push(`/messages/${item.value.sellerId}`)
+  }
+}
+
+const handleBuy = async () => {
+  if (!userStore.state.isLoggedIn) {
+    ElMessage.warning('请先登录')
+    router.push({ path: '/login', query: { redirect: route.fullPath } })
+    return
+  }
+  if (!item.value?.id) return
+  try {
+    await ElMessageBox.confirm('确定要下单这件物品吗?', '确认下单')
+    const res = await getOrders().create({ itemId: item.value.id })
+    if (res.code === 200) {
+      ElMessage.success('下单成功')
+      router.push(`/order/${res.data}`)
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  }
+}
+
+onMounted(loadItem)
+</script>
+
+<style scoped>
+.item-detail {
+  max-width: 900px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.image-carousel {
+  margin-bottom: 24px;
+  border-radius: 8px;
+  overflow: hidden;
+}
+.detail-image {
+  width: 100%;
+  height: 100%;
+}
+.detail-info {
+  padding: 0 4px;
+}
+.detail-title {
+  margin: 0 0 12px;
+  font-size: 24px;
+  font-weight: 600;
+}
+.detail-price {
+  font-size: 28px;
+  font-weight: 700;
+  color: #f56c6c;
+  margin-bottom: 16px;
+}
+.detail-meta {
+  display: flex;
+  gap: 8px;
+  margin-bottom: 16px;
+}
+.detail-desc {
+  color: #606266;
+  line-height: 1.8;
+  margin-bottom: 24px;
+  white-space: pre-wrap;
+}
+.seller-info {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 16px 0;
+  border-top: 1px solid #e4e7ed;
+  margin-bottom: 24px;
+}
+.seller-name {
+  font-size: 16px;
+  font-weight: 500;
+}
+.action-bar {
+  display: flex;
+  gap: 12px;
+}
+.action-bar .el-button {
+  flex: 1;
+}
+</style>

+ 20 - 21
src/view/LoginView.vue → src/view/Login.vue

@@ -128,14 +128,12 @@ import { useRoute, useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
 import { User, Lock, ArrowLeft } from '@element-plus/icons-vue'
 import { User, Lock, ArrowLeft } from '@element-plus/icons-vue'
 import type { FormInstance, FormRules } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
-import { getAuthController } from '../api/auth-controller'
-import type { LoginDto, RegisterDto } from '../api/models'
-import { useLoginUserStore } from '../store'
-import type { User as UserType } from '../type'
+import { getAuth } from '../api/endpoints/auth'
+import { useUserStore } from '../store'
 
 
 const route = useRoute()
 const route = useRoute()
 const router = useRouter()
 const router = useRouter()
-const loginUserStore = useLoginUserStore()
+const userStore = useUserStore()
 
 
 const activeTab = ref('login')
 const activeTab = ref('login')
 
 
@@ -197,19 +195,20 @@ const handleLogin = async () => {
 
 
   loginLoading.value = true
   loginLoading.value = true
   try {
   try {
-    const authApi = getAuthController()
+    const authApi = getAuth()
     const res = await authApi.login({
     const res = await authApi.login({
-      account: loginForm.account,
-      password: loginForm.password
-    } as LoginDto)
-
-    const tokenData = res.data
-    if (res.code === 200 && tokenData?.token) {
-      localStorage.setItem('token', tokenData.token)
-      loginUserStore.loginSuccess({
-        id: String(tokenData.userId || ''),
-        name: tokenData.username || tokenData.account || '用户',
-        role: (tokenData.role as UserType['role']) || 'user'
+      studentId: loginForm.account,
+      password: loginForm.password,
+    })
+
+    if (res.code === 200) {
+      const data = res.data!
+      userStore.login({
+        id: data.id ?? 0,
+        nickname: data.nickname || data.name || '用户',
+        avatarUrl: data.avatarUrl ?? '',
+        role: data.role || 'USER',
+        authStatus: data.authStatus || (data.role === 'ADMIN' ? 'AUTHENTICATED' : 'UNAUTHENTICATED'),
       })
       })
       ElMessage.success('登录成功')
       ElMessage.success('登录成功')
       router.push((route.query.redirect as string) || '/')
       router.push((route.query.redirect as string) || '/')
@@ -229,12 +228,12 @@ const handleRegister = async () => {
 
 
   registerLoading.value = true
   registerLoading.value = true
   try {
   try {
-    const authApi = getAuthController()
+    const authApi = getAuth()
     const res = await authApi.register({
     const res = await authApi.register({
-      account: registerForm.account,
+      studentId: registerForm.account,
+      name: registerForm.username,
       password: registerForm.password,
       password: registerForm.password,
-      username: registerForm.username
-    } as RegisterDto)
+    })
 
 
     if (res.code === 200) {
     if (res.code === 200) {
       ElMessage.success('注册成功,请登录')
       ElMessage.success('注册成功,请登录')

+ 105 - 0
src/view/Messages.vue

@@ -0,0 +1,105 @@
+<template>
+  <div class="messages-page">
+    <h1 class="page-title">消息</h1>
+
+    <div v-loading="loading">
+      <div
+        v-for="conv in conversations"
+        :key="conv.userId"
+        class="conversation-row"
+        @click="router.push(`/messages/${conv.userId}`)"
+      >
+        <el-card shadow="hover" class="conversation-card">
+          <div class="conv-content">
+            <el-badge :value="conv.unreadCount" :hidden="!conv.unreadCount">
+              <el-avatar :src="conv.avatarUrl" />
+            </el-badge>
+            <div class="conv-info">
+              <div class="conv-header">
+                <span class="conv-name">{{ conv.nickname }}</span>
+                <span class="conv-time">{{ conv.lastMessageTime }}</span>
+              </div>
+              <p class="conv-preview">{{ conv.lastMessage }}</p>
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </div>
+
+    <div v-if="!loading && conversations.length === 0" class="empty-state">
+      <el-empty description="暂无消息" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { getMessages } from '../api/endpoints/messages'
+import type { ConversationVO } from '../api/models'
+
+const router = useRouter()
+const conversations = ref<ConversationVO[]>([])
+const loading = ref(false)
+
+onMounted(async () => {
+  loading.value = true
+  try {
+    const res = await getMessages().conversations()
+    if (res.code === 200) {
+      conversations.value = res.data ?? []
+    }
+  } finally {
+    loading.value = false
+  }
+})
+</script>
+
+<style scoped>
+.messages-page {
+  max-width: 700px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.page-title {
+  font-size: 24px;
+  margin-bottom: 20px;
+}
+.conversation-row {
+  margin-bottom: 8px;
+  cursor: pointer;
+}
+.conv-content {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+.conv-info {
+  flex: 1;
+  min-width: 0;
+}
+.conv-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 4px;
+}
+.conv-name {
+  font-weight: 500;
+}
+.conv-time {
+  font-size: 12px;
+  color: #909399;
+}
+.conv-preview {
+  margin: 0;
+  font-size: 13px;
+  color: #909399;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.empty-state {
+  padding: 40px 0;
+}
+</style>

+ 185 - 0
src/view/MyItems.vue

@@ -0,0 +1,185 @@
+<template>
+  <div class="my-items">
+    <h1 class="page-title">我的发布</h1>
+
+    <el-tabs v-model="activeTab" @tab-change="loadItems">
+      <el-tab-pane label="售卖中" name="ON_SALE" />
+      <el-tab-pane label="已下架" name="OFF_SHELF" />
+      <el-tab-pane label="已售出" name="SOLD" />
+      <el-tab-pane label="审核中" name="PENDING" />
+    </el-tabs>
+
+    <div v-loading="loading">
+      <div v-for="item in itemList" :key="item.id" class="item-row">
+        <el-card shadow="hover" class="item-card">
+          <div class="item-content">
+            <el-image
+              :src="item.images?.[0]"
+              fit="cover"
+              class="item-thumb"
+            >
+              <template #error>
+                <div class="thumb-placeholder">
+                  <el-icon><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
+            <div class="item-info">
+              <h3 class="item-title">{{ item.title }}</h3>
+              <span class="item-price">¥{{ item.price }}</span>
+              <el-tag :type="statusTagType(item.status)" size="small">
+                {{ statusLabel(item.status) }}
+              </el-tag>
+            </div>
+            <div class="item-actions">
+              <el-button v-if="item.status === 'ON_SALE'" size="small" @click="handleOffline(item.id)">
+                下架
+              </el-button>
+              <el-button v-if="item.status !== 'SOLD'" size="small" type="danger" @click="handleDelete(item.id)">
+                删除
+              </el-button>
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </div>
+
+    <div v-if="!loading && itemList.length === 0" class="empty-state">
+      <el-empty description="暂无物品" />
+    </div>
+
+    
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Picture } from '@element-plus/icons-vue'
+import { getItems } from '../api/endpoints/items'
+import type { ItemVO } from '../api/models'
+
+const activeTab = ref('ON_SALE')
+const itemList = ref<ItemVO[]>([])
+const loading = ref(false)
+
+const statusLabel = (status?: string) => {
+  const map: Record<string, string> = {
+    PENDING: '审核中',
+    ON_SALE: '售卖中',
+    OFF_SHELF: '已下架',
+    SOLD: '已售出',
+  }
+  return map[status ?? ''] || (status ?? '')
+}
+
+const statusTagType = (status?: string) => {
+  const map: Record<string, string> = {
+    PENDING: 'warning',
+    ON_SALE: 'success',
+    OFF_SHELF: 'info',
+    SOLD: '',
+  }
+  return map[status ?? ''] || 'info'
+}
+
+const loadItems = async () => {
+  loading.value = true
+  try {
+    const res = await getItems().myItems()
+    if (res.code === 200) {
+      itemList.value = (res.data ?? []).filter(
+        (i) => !activeTab.value || i.status === activeTab.value,
+      )
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleOffline = async (id?: number) => {
+  if (!id) return
+  try {
+    await ElMessageBox.confirm('确定下架该物品?', '确认')
+    const res = await getItems().offline(id)
+    if (res.code === 200) {
+      ElMessage.success('已下架')
+      loadItems()
+    }
+  } catch { /* cancelled */ }
+}
+
+const handleDelete = async (id?: number) => {
+  if (!id) return
+  try {
+    await ElMessageBox.confirm('确定删除该物品?删除后不可恢复', '确认', { type: 'warning' })
+    const res = await getItems()._delete(id)
+    if (res.code === 200) {
+      ElMessage.success('已删除')
+      loadItems()
+    }
+  } catch { /* cancelled */ }
+}
+</script>
+
+<style scoped>
+.my-items {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.page-title {
+  font-size: 24px;
+  margin-bottom: 20px;
+}
+.item-row {
+  margin-bottom: 12px;
+}
+.item-content {
+  display: flex;
+  gap: 16px;
+  align-items: center;
+}
+.item-thumb {
+  width: 100px;
+  height: 100px;
+  border-radius: 4px;
+  flex-shrink: 0;
+}
+.thumb-placeholder {
+  width: 100px;
+  height: 100px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f5f7fa;
+  color: #c0c4cc;
+  border-radius: 4px;
+}
+.item-info {
+  flex: 1;
+}
+.item-title {
+  margin: 0 0 8px;
+  font-size: 16px;
+}
+.item-price {
+  font-size: 18px;
+  color: #f56c6c;
+  font-weight: 600;
+  margin-right: 12px;
+}
+.item-actions {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+.empty-state {
+  padding: 40px 0;
+}
+.pagination {
+  display: flex;
+  justify-content: center;
+  padding: 20px 0;
+}
+</style>

+ 178 - 0
src/view/MyOrders.vue

@@ -0,0 +1,178 @@
+<template>
+  <div class="my-orders">
+    <h1 class="page-title">我的订单</h1>
+
+    <el-tabs v-model="activeTab" @tab-change="loadOrders">
+      <el-tab-pane label="我买到的" name="bought" />
+      <el-tab-pane label="我卖出的" name="sold" />
+    </el-tabs>
+
+    <div v-loading="loading">
+      <div v-for="order in orderList" :key="order.id" class="order-row">
+        <el-card shadow="hover" class="order-card" @click="router.push(`/order/${order.id}`)">
+          <div class="order-content">
+            <el-image
+              :src="order.itemImage"
+              fit="cover"
+              class="order-thumb"
+            >
+              <template #error>
+                <div class="thumb-placeholder">
+                  <el-icon><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
+            <div class="order-info">
+              <h3 class="order-title">{{ order.itemTitle }}</h3>
+              <span class="order-price">¥{{ order.itemPrice }}</span>
+              <span class="order-trader">
+                {{ activeTab === 'bought' ? '卖家:' + order.sellerNickname : '买家:' + order.buyerNickname }}
+              </span>
+            </div>
+            <div class="order-status">
+              <el-tag :type="statusTagType(order.status)" size="large">
+                {{ statusLabel(order.status) }}
+              </el-tag>
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </div>
+
+    <div v-if="!loading && orderList.length === 0" class="empty-state">
+      <el-empty description="暂无订单" />
+    </div>
+
+    <el-pagination
+      v-if="total > pageSize"
+      v-model:current-page="pageNum"
+      :total="total"
+      :page-size="pageSize"
+      layout="prev, pager, next"
+      class="pagination"
+      @current-change="loadOrders"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { Picture } from '@element-plus/icons-vue'
+import { getOrders } from '../api/endpoints/orders'
+import type { OrderVO } from '../api/models'
+
+const router = useRouter()
+const activeTab = ref('bought')
+const orderList = ref<OrderVO[]>([])
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = 10
+const loading = ref(false)
+
+const statusLabel = (status?: string) => {
+  const map: Record<string, string> = {
+    PENDING: '待接单',
+    CONFIRMED: '待面交',
+    DELIVERED: '待收货',
+    COMPLETED: '已完成',
+    CANCELLED: '已取消',
+  }
+  return map[status ?? ''] || (status ?? '')
+}
+
+const statusTagType = (status?: string) => {
+  const map: Record<string, string> = {
+    PENDING: 'warning',
+    CONFIRMED: 'primary',
+    DELIVERED: 'success',
+    COMPLETED: 'success',
+    CANCELLED: 'info',
+  }
+  return map[status ?? ''] || 'info'
+}
+
+const loadOrders = async () => {
+  loading.value = true
+  try {
+    const api = getOrders()
+    const params = { pageNum: pageNum.value, pageSize }
+    const res = activeTab.value === 'bought'
+      ? await api.bought(params)
+      : await api.sold(params)
+    if (res.code === 200) {
+      orderList.value = res.data?.list ?? []
+      total.value = res.data?.total ?? 0
+    }
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
+<style scoped>
+.my-orders {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.page-title {
+  font-size: 24px;
+  margin-bottom: 20px;
+}
+.order-row {
+  margin-bottom: 12px;
+  cursor: pointer;
+}
+.order-content {
+  display: flex;
+  gap: 16px;
+  align-items: center;
+}
+.order-thumb {
+  width: 80px;
+  height: 80px;
+  border-radius: 4px;
+  flex-shrink: 0;
+}
+.thumb-placeholder {
+  width: 80px;
+  height: 80px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f5f7fa;
+  color: #c0c4cc;
+  border-radius: 4px;
+}
+.order-info {
+  flex: 1;
+}
+.order-title {
+  margin: 0 0 4px;
+  font-size: 16px;
+}
+.order-price {
+  font-size: 16px;
+  color: #f56c6c;
+  font-weight: 600;
+  margin-right: 12px;
+}
+.order-trader {
+  font-size: 13px;
+  color: #909399;
+  display: block;
+  margin-top: 4px;
+}
+.order-status {
+  flex-shrink: 0;
+}
+.empty-state {
+  padding: 40px 0;
+}
+.pagination {
+  display: flex;
+  justify-content: center;
+  padding: 20px 0;
+}
+</style>

+ 275 - 0
src/view/OrderDetail.vue

@@ -0,0 +1,275 @@
+<template>
+  <div class="order-detail">
+    <div v-loading="loading">
+      <template v-if="order">
+        <el-card class="order-card">
+          <template #header>
+            <div class="order-header">
+              <span>订单信息</span>
+              <el-tag :type="statusTagType(order.status)" size="large">
+                {{ statusLabel(order.status) }}
+              </el-tag>
+            </div>
+          </template>
+
+          <div class="item-info">
+            <el-image :src="order.itemImage" fit="cover" class="item-thumb">
+              <template #error>
+                <div class="thumb-placeholder">
+                  <el-icon><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
+            <div class="item-details">
+              <h3>{{ order.itemTitle }}</h3>
+              <span class="price">¥{{ order.itemPrice }}</span>
+              <span class="method">{{ order.tradeMethod === 'PICKUP' ? '自提' : '送货' }}</span>
+            </div>
+          </div>
+
+          <el-divider />
+
+          <div class="parties">
+            <div class="party">
+              <span class="label">买家</span>
+              <span>{{ order.buyerNickname }}</span>
+            </div>
+            <div class="party">
+              <span class="label">卖家</span>
+              <span>{{ order.sellerNickname }}</span>
+            </div>
+            <div class="party">
+              <span class="label">创建时间</span>
+              <span>{{ order.createdAt }}</span>
+            </div>
+          </div>
+
+          <el-divider />
+
+          <el-timeline>
+            <el-timeline-item
+              v-for="(step, i) in timelineSteps"
+              :key="i"
+              :type="step.active ? 'primary' : 'info'"
+              :timestamp="step.time"
+            >
+              {{ step.label }}
+            </el-timeline-item>
+          </el-timeline>
+        </el-card>
+
+        <div class="action-bar">
+          <el-button v-if="order.status === 'PENDING' && isSeller" type="primary" @click="handleAccept">
+            接单
+          </el-button>
+          <el-button v-if="order.status === 'PENDING' && isSeller" type="danger" @click="handleReject">
+            拒单
+          </el-button>
+          <el-button v-if="order.status === 'CONFIRMED' && isSeller" type="primary" @click="handleDeliver">
+            确认交货
+          </el-button>
+          <el-button v-if="order.status === 'DELIVERED' && !isSeller" type="success" @click="handleConfirm">
+            确认收货
+          </el-button>
+          <el-button @click="handleContact">
+            <el-icon><ChatDotSquare /></el-icon>联系对方
+          </el-button>
+        </div>
+      </template>
+      <el-empty v-else-if="!loading" description="订单不存在" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Picture, ChatDotSquare } from '@element-plus/icons-vue'
+import { getOrders } from '../api/endpoints/orders'
+import { useUserStore } from '../store'
+import type { OrderVO } from '../api/models'
+
+const route = useRoute()
+const router = useRouter()
+const userStore = useUserStore()
+const order = ref<OrderVO | null>(null)
+const loading = ref(false)
+
+const isSeller = computed(() => order.value?.sellerId === userStore.state.userId)
+
+const statusLabel = (status?: string) => {
+  const map: Record<string, string> = {
+    PENDING: '待接单',
+    CONFIRMED: '待面交',
+    DELIVERED: '待收货',
+    COMPLETED: '已完成',
+    CANCELLED: '已取消',
+  }
+  return map[status ?? ''] || (status ?? '')
+}
+
+const statusTagType = (status?: string) => {
+  const map: Record<string, string> = {
+    PENDING: 'warning',
+    CONFIRMED: 'primary',
+    DELIVERED: 'success',
+    COMPLETED: 'success',
+    CANCELLED: 'info',
+  }
+  return map[status ?? ''] || 'info'
+}
+
+const timelineSteps = computed(() => {
+  const steps = [
+    { label: '订单创建', time: order.value?.createdAt, active: true },
+    { label: '卖家接单', time: '', active: ['CONFIRMED', 'DELIVERED', 'COMPLETED'].includes(order.value?.status ?? '') },
+    { label: '卖家交货', time: '', active: ['DELIVERED', 'COMPLETED'].includes(order.value?.status ?? '') },
+    { label: '交易完成', time: '', active: order.value?.status === 'COMPLETED' },
+  ]
+  if (order.value?.status === 'CANCELLED') {
+    steps.push({ label: '已取消', time: order.value?.updatedAt, active: true })
+  }
+  return steps
+})
+
+const handleAccept = async () => {
+  if (!order.value?.id) return
+  try {
+    const res = await getOrders().accept(order.value.id)
+    if (res.code === 200) {
+      ElMessage.success('已接单')
+      await loadOrder()
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  }
+}
+
+const handleReject = async () => {
+  if (!order.value?.id) return
+  try {
+    await ElMessageBox.confirm('确定拒绝该订单?', '确认')
+    const res = await getOrders().reject(order.value.id)
+    if (res.code === 200) {
+      ElMessage.success('已拒单')
+      await loadOrder()
+    }
+  } catch { /* cancelled */ }
+}
+
+const handleDeliver = async () => {
+  if (!order.value?.id) return
+  try {
+    const res = await getOrders().deliver(order.value.id)
+    if (res.code === 200) {
+      ElMessage.success('已确认交货,等待买家收货')
+      await loadOrder()
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  }
+}
+
+const handleConfirm = async () => {
+  if (!order.value?.id) return
+  try {
+    await ElMessageBox.confirm('确定已收到物品?', '确认收货')
+    const res = await getOrders().confirm(order.value.id)
+    if (res.code === 200) {
+      ElMessage.success('交易完成')
+      await loadOrder()
+    }
+  } catch { /* cancelled */ }
+}
+
+const handleContact = () => {
+  if (!order.value) return
+  const targetId = isSeller.value ? order.value.buyerId : order.value.sellerId
+  if (targetId) router.push(`/messages/${targetId}`)
+}
+
+const loadOrder = async () => {
+  loading.value = true
+  try {
+    const id = Number(route.params.id)
+    const api = getOrders()
+    const [boughtRes, soldRes] = await Promise.all([api.bought(), api.sold()])
+    order.value =
+      boughtRes.data?.list?.find((o) => o.id === id) ??
+      soldRes.data?.list?.find((o) => o.id === id) ??
+      null
+  } finally {
+    loading.value = false
+  }
+}
+
+onMounted(loadOrder)
+</script>
+
+<style scoped>
+.order-detail {
+  max-width: 700px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.order-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.item-info {
+  display: flex;
+  gap: 16px;
+}
+.item-thumb {
+  width: 120px;
+  height: 120px;
+  border-radius: 4px;
+  flex-shrink: 0;
+}
+.thumb-placeholder {
+  width: 120px;
+  height: 120px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f5f7fa;
+  color: #c0c4cc;
+  border-radius: 4px;
+}
+.item-details h3 {
+  margin: 0 0 8px;
+}
+.price {
+  font-size: 20px;
+  color: #f56c6c;
+  font-weight: 600;
+  margin-right: 12px;
+}
+.method {
+  font-size: 14px;
+  color: #909399;
+}
+.parties {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+.party .label {
+  color: #909399;
+  margin-right: 12px;
+  min-width: 60px;
+  display: inline-block;
+}
+.action-bar {
+  display: flex;
+  gap: 12px;
+  margin-top: 20px;
+  flex-wrap: wrap;
+}
+</style>

+ 142 - 0
src/view/Profile.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="profile-page">
+    <h1 class="page-title">个人资料</h1>
+
+    <el-card class="profile-card">
+      <div class="avatar-section">
+        <el-avatar :size="80" :src="userStore.state.avatarUrl">
+          {{ userStore.state.nickname.charAt(0) }}
+        </el-avatar>
+        <el-button size="small" @click="triggerUpload">修改头像</el-button>
+        <input
+          ref="fileInputRef"
+          type="file"
+          accept="image/*"
+          style="display: none"
+          @change="handleAvatarChange"
+        />
+      </div>
+
+      <el-divider />
+
+      <el-form label-width="80px">
+        <el-form-item label="昵称">
+          <el-input v-model="nickname" />
+        </el-form-item>
+        <el-form-item label="个性签名">
+          <el-input v-model="signature" type="textarea" :rows="2" maxlength="200" show-word-limit />
+        </el-form-item>
+        <el-form-item label="角色">
+          <el-tag>{{ userStore.state.role === 'ADMIN' ? '管理员' : '普通用户' }}</el-tag>
+        </el-form-item>
+        <el-form-item label="认证状态">
+          <el-tag :type="authStatusTagType">
+            {{ authStatusLabel }}
+          </el-tag>
+        </el-form-item>
+      </el-form>
+
+      <el-divider />
+
+      <div class="actions">
+        <el-button type="primary" :loading="saving" @click="handleSave">保存修改</el-button>
+        <el-button @click="router.push('/profile/password')">修改密码</el-button>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { useUserStore } from '../store'
+import { getUsers } from '../api/endpoints/users'
+
+const router = useRouter()
+const userStore = useUserStore()
+const fileInputRef = ref<HTMLInputElement | null>(null)
+const nickname = ref(userStore.state.nickname)
+const signature = ref('')
+const avatarFile = ref<File | null>(null)
+const saving = ref(false)
+
+const authStatusTagType = computed(() => {
+  const map: Record<string, string> = {
+    AUTHENTICATED: 'success',
+    PENDING: 'warning',
+    UNAUTHENTICATED: 'info',
+  }
+  return map[userStore.state.authStatus] || 'info'
+})
+
+const authStatusLabel = computed(() => {
+  const map: Record<string, string> = {
+    AUTHENTICATED: '已认证',
+    PENDING: '审核中',
+    UNAUTHENTICATED: '未认证',
+  }
+  return map[userStore.state.authStatus] || userStore.state.authStatus
+})
+
+const triggerUpload = () => {
+  fileInputRef.value?.click()
+}
+
+const handleAvatarChange = (event: Event) => {
+  const target = event.target as HTMLInputElement
+  if (target.files?.length) {
+    avatarFile.value = target.files[0]
+  }
+}
+
+const handleSave = async () => {
+  saving.value = true
+  try {
+    const res = await getUsers().updateProfile({
+      nickname: nickname.value,
+      signature: signature.value,
+      ...(avatarFile.value ? { avatar: avatarFile.value } : {}),
+    })
+    if (res.code === 200) {
+      userStore.updateProfile({ nickname: nickname.value })
+      if (res.data?.avatarUrl) {
+        userStore.updateProfile({ avatarUrl: res.data.avatarUrl })
+      }
+      ElMessage.success('保存成功')
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  } finally {
+    saving.value = false
+  }
+}
+</script>
+
+<style scoped>
+.profile-page {
+  max-width: 600px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.page-title {
+  font-size: 24px;
+  margin-bottom: 20px;
+}
+.profile-card {
+  padding: 20px;
+}
+.avatar-section {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+}
+.actions {
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+}
+</style>

+ 143 - 0
src/view/Publish.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="publish-page">
+    <h1 class="page-title">发布物品</h1>
+    <el-card class="publish-card">
+      <el-form
+        ref="formRef"
+        :model="form"
+        :rules="rules"
+        label-width="100px"
+        @submit.prevent="handlePublish"
+      >
+        <el-form-item label="标题" prop="title">
+          <el-input v-model="form.title" placeholder="请输入物品标题" maxlength="100" show-word-limit />
+        </el-form-item>
+
+        <el-form-item label="描述" prop="description">
+          <el-input v-model="form.description" type="textarea" :rows="4" placeholder="请描述物品状况" />
+        </el-form-item>
+
+        <el-form-item label="价格" prop="price">
+          <el-input-number v-model="form.price" :min="0" :precision="2" :step="1" />
+        </el-form-item>
+
+        <el-form-item label="分类" prop="categoryId">
+          <el-select v-model="form.categoryId" placeholder="选择分类" style="width: 100%">
+            <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="交易方式" prop="tradeMethod">
+          <el-radio-group v-model="form.tradeMethod">
+            <el-radio value="PICKUP">自提</el-radio>
+            <el-radio value="DELIVERY">送货</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <el-form-item label="图片">
+          <el-upload
+            v-model:file-list="fileList"
+            :auto-upload="false"
+            list-type="picture-card"
+            :limit="9"
+            :multiple="true"
+            :on-preview="handlePreview"
+          >
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" :loading="submitting" size="large" @click="handlePublish">
+            发布
+          </el-button>
+          <el-button size="large" @click="router.push('/home')">取消</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+import type { FormInstance, FormRules, UploadFile } from 'element-plus'
+import { getItems } from '../api/endpoints/items'
+import { getCategories } from '../api/endpoints/categories'
+import type { Category, ItemPublishRequestTradeMethod } from '../api/models'
+
+const router = useRouter()
+const formRef = ref<FormInstance>()
+const submitting = ref(false)
+const categories = ref<Category[]>([])
+const fileList = ref<UploadFile[]>([])
+
+const form = reactive({
+  title: '',
+  description: '',
+  price: 0,
+  categoryId: undefined as number | undefined,
+  tradeMethod: 'PICKUP',
+})
+
+const rules: FormRules = {
+  title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
+  price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
+  tradeMethod: [{ required: true, message: '请选择交易方式', trigger: 'change' }],
+}
+
+const handlePreview = (file: UploadFile) => {
+  if (file.url) window.open(file.url)
+}
+
+const handlePublish = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+
+  submitting.value = true
+  try {
+    const res = await getItems().publish({
+      title: form.title,
+      description: form.description,
+      price: form.price,
+      categoryId: form.categoryId,
+      tradeMethod: form.tradeMethod as ItemPublishRequestTradeMethod,
+      images: fileList.value.filter((f) => f.raw).map((f) => f.raw!) as Blob[],
+    })
+    if (res.code === 200) {
+      ElMessage.success('发布成功,等待管理员审核')
+      router.push('/my/items')
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  } finally {
+    submitting.value = false
+  }
+}
+
+onMounted(async () => {
+  try {
+    const res = await getCategories().list1()
+    if (res.code === 200) categories.value = res.data ?? []
+  } catch { /* ignore */ }
+})
+</script>
+
+<style scoped>
+.publish-page {
+  max-width: 700px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.page-title {
+  font-size: 24px;
+  margin-bottom: 24px;
+}
+.publish-card {
+  padding: 20px;
+}
+</style>

+ 123 - 0
src/view/Register.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="register-page">
+    <el-card class="register-card">
+      <h1 class="register-title">注册</h1>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" @submit.prevent="handleRegister">
+        <el-form-item label="学号" prop="studentId">
+          <el-input v-model="form.studentId" placeholder="请输入学号" />
+        </el-form-item>
+        <el-form-item label="姓名" prop="name">
+          <el-input v-model="form.name" placeholder="请输入真实姓名" />
+        </el-form-item>
+        <el-form-item label="密码" prop="password">
+          <el-input v-model="form.password" type="password" show-password placeholder="请输入密码" />
+        </el-form-item>
+        <el-form-item label="确认密码" prop="confirmPassword">
+          <el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入密码" />
+        </el-form-item>
+        <el-form-item label="校园卡">
+          <el-upload
+            v-model:file-list="cardFileList"
+            :auto-upload="false"
+            list-type="picture-card"
+            :limit="1"
+            :on-preview="handlePreview"
+          >
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :loading="submitting" size="large" native-type="submit">
+            注册
+          </el-button>
+          <el-button size="large" @click="router.push('/login')">返回登录</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+import type { FormInstance, FormRules, UploadFile } from 'element-plus'
+import { getAuth } from '../api/endpoints/auth'
+
+const router = useRouter()
+const formRef = ref<FormInstance>()
+const submitting = ref(false)
+const cardFileList = ref<UploadFile[]>([])
+
+const form = reactive({
+  studentId: '',
+  name: '',
+  password: '',
+  confirmPassword: '',
+})
+
+const rules: FormRules = {
+  studentId: [{ required: true, message: '请输入学号', trigger: 'blur' }],
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 4, message: '密码长度至少4位', trigger: 'blur' },
+  ],
+  confirmPassword: [
+    { required: true, message: '请确认密码', trigger: 'blur' },
+    { validator: (_rule: unknown, value: string, cb: (err?: Error) => void) => {
+      value !== form.password ? cb(new Error('两次输入的密码不一致')) : cb()
+    }, trigger: 'blur' },
+  ],
+}
+
+const handlePreview = (file: UploadFile) => {
+  if (file.url) window.open(file.url)
+}
+
+const handleRegister = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+
+  submitting.value = true
+  try {
+    const res = await getAuth().register({
+      studentId: form.studentId,
+      name: form.name,
+      password: form.password,
+      campusCardImage: cardFileList.value[0]?.raw,
+    })
+    if (res.code === 200) {
+      ElMessage.success('注册成功,等待实名审核')
+      router.push('/login')
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  } finally {
+    submitting.value = false
+  }
+}
+</script>
+
+<style scoped>
+.register-page {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  background: linear-gradient(135deg, #fdf2f8 0%, #fce7f3 50%, #fbcfe8 100%);
+}
+.register-card {
+  width: 480px;
+  padding: 20px;
+}
+.register-title {
+  text-align: center;
+  margin-bottom: 24px;
+  font-size: 24px;
+  color: #831843;
+}
+</style>

+ 203 - 0
src/view/Search.vue

@@ -0,0 +1,203 @@
+<template>
+  <div class="search-page">
+    <div class="search-header">
+      <el-input
+        v-model="keyword"
+        placeholder="搜索闲置物品"
+        :prefix-icon="Search"
+        size="large"
+        clearable
+        @keyup.enter="doSearch"
+      />
+      <el-select v-model="categoryId" placeholder="分类筛选" clearable class="category-select" @change="doSearch">
+        <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id" />
+      </el-select>
+      <el-button type="primary" size="large" @click="doSearch">搜索</el-button>
+    </div>
+
+    <el-row :gutter="12" class="item-grid">
+      <el-col
+        v-for="item in itemList"
+        :key="item.id"
+        :xs="24"
+        :sm="12"
+        :md="8"
+        :lg="6"
+        class="item-col"
+      >
+        <el-card :body-style="{ padding: '0px' }" shadow="hover" class="item-card" @click="goDetail(item.id)">
+          <el-image :src="item.images?.[0]" fit="cover" class="item-image">
+            <template #error>
+              <div class="image-placeholder">
+                <el-icon :size="32"><Picture /></el-icon>
+              </div>
+            </template>
+          </el-image>
+          <div class="item-info">
+            <h4 class="item-title">{{ item.title }}</h4>
+            <div class="item-meta">
+              <span class="price">¥{{ item.price }}</span>
+              <span class="seller">{{ item.sellerNickname }}</span>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <div v-if="loading" class="loading-skeleton">
+      <el-skeleton :rows="3" animated />
+    </div>
+
+    <div v-if="!loading && itemList.length === 0" class="empty-state">
+      <el-empty description="未找到相关物品" />
+    </div>
+
+    <el-pagination
+      v-if="total > pageSize"
+      v-model:current-page="pageNum"
+      :total="total"
+      :page-size="pageSize"
+      layout="prev, pager, next"
+      class="pagination"
+      @current-change="doSearch"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { Search, Picture } from '@element-plus/icons-vue'
+import { getItems } from '../api/endpoints/items'
+import { getCategories } from '../api/endpoints/categories'
+import type { ItemVO, Category } from '../api/models'
+
+const route = useRoute()
+const router = useRouter()
+const keyword = ref('')
+const categoryId = ref<number | undefined>()
+const itemList = ref<ItemVO[]>([])
+const categories = ref<Category[]>([])
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = 10
+const loading = ref(false)
+
+const doSearch = async () => {
+  loading.value = true
+  pageNum.value = 1
+  try {
+    const api = getItems()
+    const params = { pageNum: pageNum.value, pageSize }
+    let res
+    if (keyword.value.trim()) {
+      res = await api.search({ keyword: keyword.value.trim(), ...params })
+    } else if (categoryId.value) {
+      res = await api.byCategory(categoryId.value, params)
+    } else {
+      res = await api.list(params)
+    }
+    if (res.code === 200) {
+      itemList.value = res.data?.list ?? []
+      total.value = res.data?.total ?? 0
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+const goDetail = (id?: number) => {
+  if (id) router.push(`/item/${id}`)
+}
+
+onMounted(async () => {
+  keyword.value = (route.query.keyword as string) || ''
+  if (keyword.value) {
+    doSearch()
+  }
+  try {
+    const res = await getCategories().list1()
+    if (res.code === 200) categories.value = res.data ?? []
+  } catch { /* ignore */ }
+})
+</script>
+
+<style scoped>
+.search-page {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 20px;
+}
+.search-header {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 24px;
+}
+.category-select {
+  width: 160px;
+}
+.item-grid {
+  margin-bottom: 20px;
+}
+.item-col {
+  margin-bottom: 12px;
+}
+.item-card {
+  cursor: pointer;
+  border-radius: 8px;
+  overflow: hidden;
+  transition: transform 0.2s;
+}
+.item-card:hover {
+  transform: translateY(-2px);
+}
+.item-image {
+  width: 100%;
+  height: 180px;
+  display: block;
+}
+.image-placeholder {
+  height: 180px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f5f7fa;
+  color: #c0c4cc;
+}
+.item-info {
+  padding: 12px;
+}
+.item-title {
+  margin: 0 0 8px;
+  font-size: 14px;
+  font-weight: 500;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.item-meta {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.price {
+  color: #f56c6c;
+  font-size: 16px;
+  font-weight: 600;
+}
+.seller {
+  font-size: 12px;
+  color: #909399;
+}
+.loading-skeleton {
+  padding: 20px;
+}
+.empty-state {
+  padding: 40px 0;
+}
+.pagination {
+  display: flex;
+  justify-content: center;
+  padding: 20px 0;
+}
+</style>

+ 0 - 436
src/view/UserView.vue

@@ -1,436 +0,0 @@
-<script setup lang="ts">
-import { getUserController } from '@/api/user-controller'
-import type { QueryByPageParams, UserDto, UserVo } from '@/api/models'
-import { ElMessage, ElMessageBox, type FormInstance, type FormRules, ElIcon } from 'element-plus'
-import { Plus, RefreshRight, Search, Delete, Edit } from '@element-plus/icons-vue'
-import { ref, reactive, computed } from 'vue'
-
-const api = getUserController()
-
-// 搜索表单
-const searchFormRef = ref<FormInstance>()
-const searchForm = reactive({
-  username: '',
-  account: '',
-  role: '',
-  enable: 1
-})
-
-// 用户表单(新增/编辑)
-const dialogVisible = ref(false)
-const dialogTitle = ref('新增用户')
-const formRef = ref<FormInstance>()
-const userForm = reactive<Partial<UserDto>>({
-  id: undefined,
-  username: '',
-  account: '',
-  password: '',
-  role: ''
-})
-const formRules: FormRules = {
-  username: [
-    { required: true, message: '请输入用户名', trigger: 'blur' },
-    { min: 2, max: 32, message: '用户名长度需在 2 到 32 个字符之间', trigger: 'blur' }
-  ],
-  account: [
-    { required: true, message: '请输入账号', trigger: 'blur' },
-    { min: 4, max: 20, message: '账号长度需在 4 到 20 个字符之间', trigger: 'blur' }
-  ],
-  password: [
-    { required: true, message: '请输入密码', trigger: 'blur' },
-    { min: 4, max: 20, message: '密码长度需在 4 到 20 个字符之间', trigger: 'blur' }
-  ],
-  role: [
-    { required: true, message: '请选择角色', trigger: 'change' },
-    { min: 4, max: 32, message: '角色长度需在 4 到 32 个字符之间', trigger: 'change' }
-  ]
-}
-
-// 表格数据
-const tableData = ref<UserVo[]>([])
-const loading = ref(false)
-const selectedRows = ref<UserVo[]>([])
-
-// 分页
-const pagination = reactive({
-  pageNum: 1,
-  pageSize: 10,
-  total: 0
-})
-
-// 角色选项
-const roleOptions = [
-  { label: '管理员', value: 'admin' },
-  { label: '普通用户', value: 'user' },
-  // { label: '访客', value: 'guest' }
-]
-
-// 是否编辑模式
-const isEdit = computed(() => !!userForm.id)
-
-// 搜索
-const handleSearch = () => {
-  pagination.pageNum = 1
-  fetchData()
-}
-
-// 重置搜索
-const handleReset = () => {
-  searchFormRef.value?.resetFields()
-  handleSearch()
-}
-
-// 获取数据
-const fetchData = async () => {
-  loading.value = true
-  try {
-    const condition: QueryByPageParams = {
-      username: searchForm.username || '',
-      account: searchForm.account || '',
-      role: searchForm.role || '',
-      enable: searchForm.enable,
-      pageNum: pagination.pageNum,
-      pageSize: pagination.pageSize
-    }
-    const res = await api.queryByPage(condition)
-    const pageData = res?.data
-    tableData.value = pageData?.data || []
-    pagination.total = pageData?.total || 0
-  } catch (err) {
-    ElMessage.error('获取数据失败')
-    tableData.value = []
-  } finally {
-    loading.value = false
-  }
-}
-
-// 翻页
-const handlePageChange = (page: number) => {
-  pagination.pageNum = page
-  fetchData()
-}
-
-// 每页条数变化
-const handleSizeChange = (size: number) => {
-  pagination.pageSize = size
-  pagination.pageNum = 1
-  fetchData()
-}
-
-// 打开新增
-const handleAdd = () => {
-  dialogTitle.value = '新增用户'
-  Object.assign(userForm, { id: undefined, username: '', account: '', password: '', role: '' })
-  dialogVisible.value = true
-}
-
-// 打开编辑
-const handleEdit = async (row: UserVo) => {
-  dialogTitle.value = '编辑用户'
-  try {
-    const id = row.id || ''
-    const res = await api.queryById(id)
-    if (res?.code === 0 || res?.code === 200) {
-      Object.assign(userForm, {
-        id: res.data?.id,
-        username: res.data?.username,
-        account: res.data?.account,
-        password: '',
-        role: res.data?.role
-      })
-      dialogVisible.value = true
-    }
-  } catch {
-    ElMessage.error('获取用户信息失败')
-  }
-}
-
-// 保存
-const handleSave = async () => {
-  await formRef.value?.validate(async (valid) => {
-    if (!valid) return
-    try {
-      const baseData = {
-        username: userForm.username!,
-        account: userForm.account!,
-        role: userForm.role!
-      }
-      
-      if (isEdit.value) {
-        const data: Partial<UserDto> = {
-          id: userForm.id,
-          ...baseData,
-          ...(userForm.password ? { password: userForm.password } : {})
-        }
-        const res = await api.edit(data as UserDto)
-        if (res?.code === 0 || res?.code === 200) {
-          ElMessage.success('修改成功')
-          dialogVisible.value = false
-          fetchData()
-        } else {
-          ElMessage.error(res?.message || '修改失败')
-        }
-      } else {
-        if (!userForm.password) {
-          ElMessage.warning('请输入密码')
-          return
-        }
-        const data: UserDto = {
-          ...baseData,
-          password: userForm.password!
-        }
-        const res = await api.add(data)
-        if (res?.code === 0 || res?.code === 200) {
-          ElMessage.success('新增成功')
-          dialogVisible.value = false
-          fetchData()
-        } else {
-          ElMessage.error(res?.message || '新增失败')
-        }
-      }
-    } catch {
-      ElMessage.error(isEdit.value ? '修改失败' : '新增失败')
-    }
-  })
-}
-
-// 对话框关闭回调,重置表单验证
-const handleDialogClosed = () => {
-  formRef.value?.resetFields()
-}
-
-// 删除
-const handleDelete = (row: UserVo) => {
-  ElMessageBox.confirm('确认删除该用户吗?', '提示', {
-    type: 'warning'
-  }).then(async () => {
-    try {
-      const ids = row.id ? [row.id] : []
-      const res = await api.deleteById({ ids })
-      if (res?.code === 0 || res?.code === 200) {
-        ElMessage.success('删除成功')
-        fetchData()
-      } else {
-        ElMessage.error(res?.message || '删除失败')
-      }
-    } catch {
-      ElMessage.error('删除失败')
-    }
-  }).catch(() => {})
-}
-
-// 批量删除
-const handleBatchDelete = () => {
-  if (!selectedRows.value.length) {
-    ElMessage.warning('请选择要删除的数据')
-    return
-  }
-  ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条数据吗?`, '提示', {
-    type: 'warning'
-  }).then(async () => {
-    try {
-      const ids = selectedRows.value.map(row => row.id).filter((id): id is string => id !== undefined)
-      const res = await api.deleteById({ ids })
-      if (res?.code === 0 || res?.code === 200) {
-        ElMessage.success('删除成功')
-        selectedRows.value = []
-        fetchData()
-      } else {
-        ElMessage.error(res?.message || '删除失败')
-      }
-    } catch {
-      ElMessage.error('删除失败')
-    }
-  }).catch(() => {})
-}
-
-
-// 切换启用状态
-const handleToggleEnable = async (row: UserVo, newEnable: number) => {
-  const oldEnable = row.enable
-  row.enable = newEnable
-  try {
-    const res = await api.updateUserStatus({ id: row.id!, enable: newEnable })
-    if (res?.code === 0 || res?.code === 200) {
-      ElMessage.success('状态更新成功')
-    } else {
-      row.enable = oldEnable
-      ElMessage.error(res?.message || '状态更新失败')
-    }
-  } catch {
-    row.enable = oldEnable
-    ElMessage.error('状态更新失败')
-  }
-}
-
-// 表格选择
-const handleSelectionChange = (rows: UserVo[]) => {
-  selectedRows.value = rows
-}
-
-// 初始加载
-fetchData()
-</script>
-
-<template>
-  <div class="user-view">
-    <!-- 搜索区域 -->
-    <el-card class="search-card" shadow="never">
-      <el-form ref="searchFormRef" :model="searchForm" inline>
-        <el-form-item label="用户名" prop="username">
-          <el-input v-model="searchForm.username" placeholder="请输入用户名" clearable style="width: 180px" />
-        </el-form-item>
-        <el-form-item label="账号" prop="account">
-          <el-input v-model="searchForm.account" placeholder="请输入账号" clearable style="width: 180px" />
-        </el-form-item>
-        <el-form-item label="角色" prop="role">
-          <el-select v-model="searchForm.role" placeholder="请选择角色" clearable style="width: 150px">
-            <el-option v-for="item in roleOptions" :key="item.value" :label="item.label" :value="item.value" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="状态" prop="enable">
-          <el-select v-model="searchForm.enable" placeholder="请选择状态" clearable style="width: 120px">
-            <el-option label="启用" :value="1" />
-            <el-option label="禁用" :value="0" />
-          </el-select>
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" @click="handleSearch">
-          <el-icon class="el-icon--left"><Search /></el-icon>搜索
-        </el-button>
-        <el-button @click="handleReset">
-          <el-icon class="el-icon--left"><RefreshRight /></el-icon>重置
-        </el-button>
-        </el-form-item>
-      </el-form>
-    </el-card>
-
-    <!-- 操作按钮 -->
-    <div class="toolbar">
-      <el-button type="primary" @click="handleAdd">
-          <el-icon class="el-icon--left"><Plus /></el-icon>新增
-        </el-button>
-        <el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
-          <el-icon class="el-icon--left"><Delete /></el-icon>批量删除
-        </el-button>
-    </div>
-
-    <!-- 表格 -->
-    <el-card class="table-card" shadow="never">
-      <el-table
-        v-loading="loading"
-        :data="tableData"
-        border
-        stripe
-        @selection-change="handleSelectionChange"
-        style="width: 100%"
-      >
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column prop="id" label="ID" width="80" align="center" />
-        <el-table-column prop="username" label="用户名" min-width="120" align="center" />
-        <el-table-column prop="account" label="账号" min-width="150" align="center" />
-        <el-table-column prop="role" label="角色" width="120" align="center">
-          <template #default="{ row }">
-            <el-tag :type="row.role === 'admin' ? 'danger' : row.role === 'user' ? 'success' : 'info'">
-              {{ roleOptions.find(t => t.value === row.role)?.label || row.role }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="enable" label="状态" width="100" align="center">
-          <template #default="{ row }">
-            <el-switch
-              :model-value="row.enable"
-              :active-value="1"
-              :inactive-value="0"
-              @change="(val: number) => handleToggleEnable(row, val)"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column prop="avatar" label="头像" width="100" align="center">
-          <template #default="{ row }">
-            <el-avatar v-if="row.avatar" :src="row.avatar" :size="40" />
-            <el-avatar v-else :size="40">{{ row.username?.charAt(0) || '?' }}</el-avatar>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="180" align="center" fixed="right">
-          <template #default="{ row }">
-            <el-button type="primary" link @click="handleEdit(row)">
-              <el-icon><Edit /></el-icon>编辑
-            </el-button>
-            <el-button type="danger" link @click="handleDelete(row)">
-              <el-icon><Delete /></el-icon>删除
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <!-- 分页 -->
-      <div class="pagination">
-        <el-pagination
-          v-model:current-page="pagination.pageNum"
-          v-model:page-size="pagination.pageSize"
-          :page-sizes="[10, 20, 50, 100]"
-          :total="pagination.total"
-          layout="total, sizes, prev, pager, next, jumper"
-          @current-change="handlePageChange"
-          @size-change="handleSizeChange"
-        />
-      </div>
-    </el-card>
-
-    <!-- 新增/编辑弹窗 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" draggable @closed="handleDialogClosed">
-      <el-form ref="formRef" :model="userForm" :rules="formRules" label-width="80px">
-        <el-form-item label="用户名" prop="username">
-          <el-input v-model="userForm.username" placeholder="请输入用户名" />
-        </el-form-item>
-        <el-form-item label="账号" prop="account">
-          <el-input v-model="userForm.account" placeholder="请输入账号" :disabled="isEdit" />
-        </el-form-item>
-        <el-form-item label="密码" :prop="isEdit ? '' : 'password'">
-          <el-input v-model="userForm.password" type="password" placeholder="请输入密码" show-password />
-          <span v-if="isEdit" class="tip">留空则不修改密码</span>
-        </el-form-item>
-        <el-form-item label="角色" prop="role">
-          <el-select v-model="userForm.role" placeholder="请选择角色" style="width: 100%">
-            <el-option v-for="item in roleOptions" :key="item.value" :label="item.label" :value="item.value" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="handleSave">确定</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<style scoped>
-.user-view {
-  padding: 20px;
-}
-
-.search-card {
-  margin-bottom: 16px;
-}
-
-.toolbar {
-  margin-bottom: 16px;
-}
-
-.table-card {
-  margin-bottom: 16px;
-}
-
-.pagination {
-  display: flex;
-  justify-content: flex-end;
-  margin-top: 16px;
-}
-
-.tip {
-  font-size: 12px;
-  color: #909399;
-  line-height: 1.4;
-}
-</style>

+ 157 - 0
src/view/admin/CategoryManage.vue

@@ -0,0 +1,157 @@
+<template>
+  <div class="category-manage">
+    <h1 class="page-title">分类管理</h1>
+
+    <div class="toolbar">
+      <el-button type="primary" @click="openCreateDialog">
+        <el-icon><Plus /></el-icon>新增分类
+      </el-button>
+    </div>
+
+    <el-table v-loading="loading" :data="categories" stripe style="width: 100%">
+      <el-table-column prop="sortOrder" label="排序" width="80" />
+      <el-table-column prop="name" label="分类名称" min-width="200" />
+      <el-table-column label="状态" width="120">
+        <template #default="{ row }">
+          <el-switch
+            :model-value="row.status === 'ENABLED'"
+            @change="handleToggleStatus(row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="200">
+        <template #default="{ row }">
+          <el-button size="small" @click="openEditDialog(row)">编辑</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog
+      v-model="dialog.visible"
+      :title="dialog.isEdit ? '编辑分类' : '新增分类'"
+      width="400"
+    >
+      <el-form ref="formRef" :model="dialog.form" :rules="rules" label-width="80px">
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="dialog.form.name" />
+        </el-form-item>
+        <el-form-item label="排序" prop="sortOrder">
+          <el-input-number v-model="dialog.form.sortOrder" :min="0" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialog.visible = false">取消</el-button>
+        <el-button type="primary" :loading="saving" @click="handleSave">保存</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { getCategories } from '../../api/endpoints/categories'
+import type { Category } from '../../api/models'
+
+const loading = ref(false)
+const saving = ref(false)
+const formRef = ref<FormInstance>()
+const categories = ref<Category[]>([])
+
+const dialog = reactive({
+  visible: false,
+  isEdit: false,
+  editId: 0,
+  form: {
+    name: '',
+    sortOrder: 0,
+  },
+})
+
+const rules: FormRules = {
+  name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
+}
+
+const loadCategories = async () => {
+  loading.value = true
+  try {
+    const res = await getCategories().list1()
+    if (res.code === 200) {
+      categories.value = res.data ?? []
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+const openCreateDialog = () => {
+  dialog.isEdit = false
+  dialog.form.name = ''
+  dialog.form.sortOrder = 0
+  dialog.visible = true
+}
+
+const openEditDialog = (row: Category) => {
+  dialog.isEdit = true
+  dialog.editId = row.id ?? 0
+  dialog.form.name = row.name ?? ''
+  dialog.form.sortOrder = row.sortOrder ?? 0
+  dialog.visible = true
+}
+
+const handleSave = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+
+  saving.value = true
+  try {
+    if (dialog.isEdit) {
+      const res = await getCategories().update(dialog.editId, { name: dialog.form.name, sortOrder: dialog.form.sortOrder })
+      if (res.code === 200) ElMessage.success('修改成功')
+    } else {
+      const res = await getCategories().create1({ name: dialog.form.name, sortOrder: dialog.form.sortOrder })
+      if (res.code === 200) ElMessage.success('新增成功')
+    }
+    dialog.visible = false
+    loadCategories()
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  } finally {
+    saving.value = false
+  }
+}
+
+const handleToggleStatus = async (row: Category) => {
+  const newStatus = row.status === 'ENABLED' ? 'DISABLED' : 'ENABLED'
+  try {
+    const res = await getCategories().toggleStatus(row.id ?? 0, { status: newStatus })
+    if (res.code === 200) {
+      ElMessage.success(newStatus === 'ENABLED' ? '已启用' : '已禁用')
+      loadCategories()
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  }
+}
+
+onMounted(loadCategories)
+</script>
+
+<style scoped>
+.category-manage {
+  padding: 20px;
+}
+.page-title {
+  font-size: 22px;
+  margin-bottom: 20px;
+}
+.toolbar {
+  margin-bottom: 16px;
+}
+</style>

+ 95 - 0
src/view/admin/Login.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="admin-login-page">
+    <el-card class="login-card">
+      <h1 class="login-title">管理员登录</h1>
+      <el-form ref="formRef" :model="form" :rules="rules" @submit.prevent="handleLogin">
+        <el-form-item prop="studentId">
+          <el-input v-model="form.studentId" placeholder="学号" :prefix-icon="User" size="large" />
+        </el-form-item>
+        <el-form-item prop="password">
+          <el-input v-model="form.password" type="password" show-password placeholder="密码" :prefix-icon="Lock" size="large" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :loading="loading" size="large" native-type="submit" class="login-btn">
+            登录
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { User, Lock } from '@element-plus/icons-vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { getAuth } from '../../api/endpoints/auth'
+import { useUserStore } from '../../store'
+
+const router = useRouter()
+const formRef = ref<FormInstance>()
+const loading = ref(false)
+const userStore = useUserStore()
+
+const form = reactive({
+  studentId: '',
+  password: '',
+})
+
+const rules: FormRules = {
+  studentId: [{ required: true, message: '请输入学号', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+}
+
+const handleLogin = async () => {
+  const valid = await formRef.value?.validate().catch(() => false)
+  if (!valid) return
+
+  loading.value = true
+  try {
+    const res = await getAuth().login({ studentId: form.studentId, password: form.password })
+    if (res.code === 200 && res.data) {
+      userStore.login({
+        id: res.data.id ?? 0,
+        nickname: res.data.nickname || res.data.name || '管理员',
+        avatarUrl: res.data.avatarUrl ?? '',
+        role: res.data.role || 'ADMIN',
+        authStatus: res.data.authStatus || 'AUTHENTICATED',
+      })
+      ElMessage.success('登录成功')
+      router.push('/admin/review/user')
+    }
+  } catch (err: unknown) {
+    if (err && typeof err === 'object' && 'message' in err) {
+      ElMessage.error((err as { message: string }).message)
+    }
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
+<style scoped>
+.admin-login-page {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 100vh;
+  background: #304156;
+}
+.login-card {
+  width: 400px;
+  padding: 20px;
+}
+.login-title {
+  text-align: center;
+  margin-bottom: 24px;
+  font-size: 22px;
+  color: #303133;
+}
+.login-btn {
+  width: 100%;
+}
+</style>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio