feat(商品管理): 实现商品管理模块的完整功能
新增商品管理相关页面和功能,包括: 1. 商品列表展示、搜索和分页 2. 商品新增、编辑和详情页面 3. 商品图片上传和压缩功能 4. 商品状态管理和操作 5. 相关API接口文档和类型定义 调整路由配置和权限设置以支持新功能
This commit is contained in:
parent
78a228507e
commit
2e7e2366f5
|
|
@ -2,7 +2,8 @@
|
|||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(tree -L 3 -I 'node_modules|dist')"
|
||||
"Bash(tree -L 3 -I 'node_modules|dist')",
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,295 @@
|
|||
# ManageGoodsController 接口文档
|
||||
|
||||
## 控制器概述
|
||||
|
||||
**基础路径:** `/api/manage/goods`
|
||||
|
||||
**控制器类:** `com.agileboot.api.controller.manage.ManageGoodsController`
|
||||
|
||||
**功能描述:** 商品管理接口,提供商品的增删改查功能。
|
||||
|
||||
---
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 获取商品列表
|
||||
|
||||
**接口路径:** `GET /api/manage/goods/list`
|
||||
|
||||
**功能描述:** 分页查询商品列表,支持多条件筛选和排序。
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| `pageNum` | Integer | 否 | 页码,默认1,最大值200 | `1` |
|
||||
| `pageSize` | Integer | 否 | 每页大小,默认10,最大值500 | `10` |
|
||||
| `orderColumn` | String | 否 | 排序字段 | `"createTime"` |
|
||||
| `orderDirection` | String | 否 | 排序方向:`"ascending"`(升序) 或 `"descending"`(降序) | `"descending"` |
|
||||
| `beginTime` | Date | 否 | 开始时间(格式:yyyy-MM-dd) | `"2025-01-01"` |
|
||||
| `endTime` | Date | 否 | 结束时间(格式:yyyy-MM-dd) | `"2025-12-31"` |
|
||||
| `goodsName` | String | 否 | 商品名称(模糊查询) | `"测试商品"` |
|
||||
| `categoryId` | Long | 否 | 商品分类ID | `1` |
|
||||
| `externalGoodsId` | Long | 否 | 外部归属类型的商品ID | `1001` |
|
||||
| `corpid` | String | 否 | 企业微信id | `"wxcorpid123"` |
|
||||
| `status` | Integer | 否 | 商品状态(1上架 2下架) | `1` |
|
||||
| `autoApproval` | Integer | 否 | 免审批(0否 1是) | `1` |
|
||||
| `minPrice` | BigDecimal | 否 | 最低价格 | `10.00` |
|
||||
| `maxPrice` | BigDecimal | 否 | 最高价格 | `100.00` |
|
||||
| `belongType` | Integer | 否 | 归属类型(0-借还柜 1-固资通) | `0` |
|
||||
|
||||
**返回类型:** `ResponseDTO<PageDTO<ShopGoodsDTO>>`
|
||||
|
||||
**响应字段:**
|
||||
|
||||
`ResponseDTO` 通用结构:
|
||||
- `code`: Integer - 响应码
|
||||
- `msg`: String - 响应消息
|
||||
- `data`: `PageDTO<ShopGoodsDTO>` - 分页数据
|
||||
|
||||
`PageDTO` 分页结构:
|
||||
- `total`: Long - 总记录数
|
||||
- `rows`: `List<ShopGoodsDTO>` - 当前页数据列表
|
||||
|
||||
`ShopGoodsDTO` 商品详情字段:
|
||||
|
||||
| 字段名 | 类型 | 描述 |
|
||||
|--------|------|------|
|
||||
| `goodsId` | Long | 商品唯一ID |
|
||||
| `goodsName` | String | 商品名称 |
|
||||
| `categoryId` | Long | 分类ID |
|
||||
| `externalGoodsId` | Long | 外部商品ID |
|
||||
| `corpid` | String | 企业微信id |
|
||||
| `categoryName` | String | 分类名称 |
|
||||
| `belongType` | Long | 归属类型(0-借还柜 1-固资通) |
|
||||
| `price` | BigDecimal | 销售价格 |
|
||||
| `stock` | Integer | 库存数量 |
|
||||
| `status` | Integer | 商品状态(1上架 2下架) |
|
||||
| `autoApproval` | Integer | 免审批(0否 1是) |
|
||||
| `coverImg` | String | 封面图URL |
|
||||
| `creatorId` | Long | 创建者ID |
|
||||
| `creatorName` | String | 创建者名称 |
|
||||
| `createTime` | Date | 创建时间 |
|
||||
| `remark` | String | 备注 |
|
||||
| `cabinetName` | String | 柜机名称 |
|
||||
| `cellNo` | Integer | 格口号 |
|
||||
| `cellNoStr` | String | 格口号(字符串格式) |
|
||||
| `totalStock` | Integer | 已分配库存 |
|
||||
| `shopNameStr` | String | 地址名称 |
|
||||
| `usageInstruction` | String | 商品使用说明 |
|
||||
| `monthlyPurchaseLimit` | Integer | 每人每月限购数量 |
|
||||
|
||||
**示例请求:**
|
||||
```
|
||||
GET /api/manage/goods/list?pageNum=1&pageSize=10&status=1&goodsName=测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 新增商品
|
||||
|
||||
**接口路径:** `POST /api/manage/goods`
|
||||
|
||||
**功能描述:** 创建新商品。
|
||||
|
||||
**请求体类型:** `AddGoodsCommand` (继承自 `ShopGoodsEntity`)
|
||||
|
||||
**请求字段:**
|
||||
|
||||
| 字段名 | 类型 | 必填 | 描述 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| `goodsName` | String | 是 | 商品名称 | `"测试商品"` |
|
||||
| `categoryId` | Long | 否 | 商品分类ID | `1` |
|
||||
| `externalGoodsId` | Long | 否 | 外部归属类型的商品ID | `1001` |
|
||||
| `corpid` | String | 否 | 企业微信id | `"wxcorpid123"` |
|
||||
| `monthlyPurchaseLimit` | Integer | 否 | 每人每月限购数量 | `5` |
|
||||
| `price` | BigDecimal | 是 | 销售价格 | `99.99` |
|
||||
| `stock` | Integer | 否 | 库存数量 | `100` |
|
||||
| `status` | Integer | 否 | 商品状态(1上架 2下架),默认值需确认 | `1` |
|
||||
| `autoApproval` | Integer | 否 | 免审批(0否 1是),默认值需确认 | `0` |
|
||||
| `coverImg` | String | 否 | 封面图URL | `"https://example.com/image.jpg"` |
|
||||
| `goodsDetail` | String | 否 | 商品详情(支持2000汉字+10个图片链接) | `"<p>商品描述</p>"` |
|
||||
| `remark` | String | 否 | 备注 | `"测试商品备注"` |
|
||||
| `usageInstruction` | String | 否 | 商品使用说明 | `"使用说明内容"` |
|
||||
| `belongType` | Integer | 否 | 归属类型(0-借还柜 1-固资通) | `0` |
|
||||
|
||||
**注意:** `goodsId` 字段为自增主键,请求时无需提供。
|
||||
|
||||
**返回类型:** `ResponseDTO<Void>`
|
||||
|
||||
**响应字段:**
|
||||
- `code`: Integer - 响应码
|
||||
- `msg`: String - 响应消息
|
||||
- `data`: null
|
||||
|
||||
**示例请求:**
|
||||
```json
|
||||
{
|
||||
"goodsName": "测试商品",
|
||||
"price": 99.99,
|
||||
"stock": 100,
|
||||
"status": 1,
|
||||
"coverImg": "https://example.com/image.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 删除商品
|
||||
|
||||
**接口路径:** `DELETE /api/manage/goods/{goodsIds}`
|
||||
|
||||
**功能描述:** 批量删除商品。
|
||||
|
||||
**路径参数:**
|
||||
- `goodsIds`: 商品ID列表,多个ID用逗号分隔
|
||||
|
||||
**请求示例:**
|
||||
```
|
||||
DELETE /api/manage/goods/1,2,3
|
||||
```
|
||||
|
||||
**内部处理:** 控制器将路径参数转换为 `BulkOperationCommand<Long>` 对象:
|
||||
```java
|
||||
{
|
||||
"ids": [1, 2, 3] // Set<Long> 类型,自动去重
|
||||
}
|
||||
```
|
||||
|
||||
**返回类型:** `ResponseDTO<Void>`
|
||||
|
||||
**响应字段:**
|
||||
- `code`: Integer - 响应码
|
||||
- `msg`: String - 响应消息
|
||||
- `data`: null
|
||||
|
||||
---
|
||||
|
||||
### 4. 修改商品
|
||||
|
||||
**接口路径:** `PUT /api/manage/goods/{goodsId}`
|
||||
|
||||
**功能描述:** 更新商品信息。
|
||||
|
||||
**路径参数:**
|
||||
- `goodsId`: 商品ID
|
||||
|
||||
**请求体类型:** `UpdateGoodsCommand` (继承自 `AddGoodsCommand`)
|
||||
|
||||
**请求字段:** 与 `AddGoodsCommand` 相同,所有字段均为可选更新。
|
||||
|
||||
**特殊处理:** 接口会自动将路径参数中的 `goodsId` 设置到 command 对象中。
|
||||
|
||||
**返回类型:** `ResponseDTO<Void>`
|
||||
|
||||
**响应字段:**
|
||||
- `code`: Integer - 响应码
|
||||
- `msg`: String - 响应消息
|
||||
- `data`: null
|
||||
|
||||
**示例请求:**
|
||||
```
|
||||
PUT /api/manage/goods/1
|
||||
```
|
||||
```json
|
||||
{
|
||||
"goodsName": "更新后的商品名称",
|
||||
"price": 88.88
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 获取单个商品信息
|
||||
|
||||
**接口路径:** `GET /api/manage/goods/getGoodsInfo`
|
||||
|
||||
**功能描述:** 根据商品ID获取单个商品的详细信息。
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 | 示例 |
|
||||
|--------|------|------|------|------|
|
||||
| `goodsId` | Long | 是 | 商品ID | `1` |
|
||||
|
||||
**返回类型:** `ResponseDTO<ShopGoodsDTO>`
|
||||
|
||||
**响应字段:**
|
||||
- `code`: Integer - 响应码
|
||||
- `msg`: String - 响应消息
|
||||
- `data`: `ShopGoodsDTO` - 商品详情(字段同接口1)
|
||||
|
||||
**示例请求:**
|
||||
```
|
||||
GET /api/manage/goods/getGoodsInfo?goodsId=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 通用数据结构
|
||||
|
||||
### ResponseDTO 通用响应结构
|
||||
```java
|
||||
public class ResponseDTO<T> {
|
||||
private Integer code; // 响应码
|
||||
private String msg; // 响应消息
|
||||
private T data; // 响应数据
|
||||
}
|
||||
```
|
||||
|
||||
### PageDTO 分页结构
|
||||
```java
|
||||
public class PageDTO<T> {
|
||||
private Long total; // 总记录数
|
||||
private List<T> rows; // 当前页数据列表
|
||||
}
|
||||
```
|
||||
|
||||
### BulkOperationCommand 批量操作命令
|
||||
```java
|
||||
public class BulkOperationCommand<T> {
|
||||
private Set<T> ids; // 操作ID集合(自动去重)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **字段默认值:** 部分字段(如 `status`, `autoApproval`)的默认值需查看业务逻辑确认
|
||||
2. **ID 生成:** `goodsId` 为数据库自增字段,创建时无需提供
|
||||
3. **批量删除:** 删除接口支持批量操作,ID列表会自动去重
|
||||
4. **分页限制:** 最大页码200,每页最大大小500
|
||||
5. **时间格式:** 时间参数需使用 `yyyy-MM-dd` 格式
|
||||
6. **企业微信集成:** `corpid` 字段用于企业微信多租户支持
|
||||
|
||||
---
|
||||
|
||||
## 相关实体类位置
|
||||
|
||||
| 类名 | 路径 |
|
||||
|------|------|
|
||||
| `ManageGoodsController` | `agileboot-api/src/main/java/com/agileboot/api/controller/manage/` |
|
||||
| `ShopGoodsEntity` | `agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/db/` |
|
||||
| `AddGoodsCommand` | `agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/command/` |
|
||||
| `UpdateGoodsCommand` | `agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/command/` |
|
||||
| `ShopGoodsDTO` | `agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/dto/` |
|
||||
| `SearchShopGoodsQuery` | `agileboot-domain/src/main/java/com/agileboot/domain/shop/goods/query/` |
|
||||
| `BulkOperationCommand` | `agileboot-domain/src/main/java/com/agileboot/domain/common/command/` |
|
||||
|
||||
---
|
||||
|
||||
## 架构设计说明
|
||||
|
||||
本控制器遵循项目的 **DDD/CQRS 架构**:
|
||||
|
||||
- **查询操作**(GET):使用 `SearchShopGoodsQuery` 对象,通过 `GoodsApplicationService.getGoodsList()` 处理
|
||||
- **命令操作**(POST/PUT/DELETE):使用 `AddGoodsCommand`/`UpdateGoodsCommand`/`BulkOperationCommand`,通过 `GoodsApplicationService` 的对应方法处理
|
||||
- **数据流转**:Controller → Command/Query → ApplicationService → Domain Model → DB Entity
|
||||
|
||||
**注意**:`AddGoodsCommand` 和 `UpdateGoodsCommand` 都继承自 `ShopGoodsEntity`,因此共享相同的字段定义。在实际使用中,`goodsId` 字段在新增时由数据库自增生成,在更新时通过路径参数传入。
|
||||
|
||||
---
|
||||
|
||||
*文档生成时间:2025-12-11*
|
||||
*基于代码分析:`agileboot-api/src/main/java/com/agileboot/api/controller/manage/ManageGoodsController.java`*
|
||||
|
|
@ -2,36 +2,52 @@ import { request } from "@/http/axios"
|
|||
import { PageDTO, ResponseData, BasePageQuery } from "../type"
|
||||
|
||||
export interface ShopGoodsDTO {
|
||||
goodsId?: number
|
||||
goodsName?: string
|
||||
categoryId?: number
|
||||
categoryName?: string
|
||||
price?: number
|
||||
stock?: number
|
||||
status?: number
|
||||
autoApproval?: number
|
||||
coverImg?: string
|
||||
creatorId?: number
|
||||
creatorName?: string
|
||||
createTime?: string
|
||||
remark?: string
|
||||
cabinetName?: string
|
||||
cellNo?: number
|
||||
cellNoStr?: string
|
||||
totalStock?: number
|
||||
usageInstruction?: string
|
||||
goodsId?: number // 商品唯一ID
|
||||
goodsName?: string // 商品名称
|
||||
categoryId?: number // 分类ID
|
||||
externalGoodsId?: number // 外部归属类型的商品ID
|
||||
corpid?: string // 企业微信id
|
||||
categoryName?: string // 分类名称
|
||||
belongType?: number // 归属类型(0-借还柜 1-固资通)
|
||||
price?: number // 销售价格
|
||||
stock?: number // 库存数量
|
||||
status?: number // 商品状态(1上架 2下架)
|
||||
autoApproval?: number // 免审批(0否 1是)
|
||||
coverImg?: string // 封面图URL
|
||||
creatorId?: number // 创建者ID
|
||||
creatorName?: string // 创建者名称
|
||||
createTime?: string // 创建时间
|
||||
remark?: string // 备注
|
||||
cabinetName?: string // 柜机名称
|
||||
cellNo?: number // 格口号
|
||||
cellNoStr?: string // 格口号(字符串格式)
|
||||
totalStock?: number // 已分配库存
|
||||
shopNameStr?: string // 地址名称
|
||||
usageInstruction?: string // 商品使用说明
|
||||
monthlyPurchaseLimit?: number // 每人每月限购数量
|
||||
goodsDetail?: string // 商品详情(支持2000汉字+10个图片链接)
|
||||
}
|
||||
|
||||
export interface SearchShopGoodsQuery extends BasePageQuery {
|
||||
goodsName?: string
|
||||
categoryId?: number
|
||||
status?: number
|
||||
autoApproval?: number
|
||||
minPrice?: number
|
||||
maxPrice?: number
|
||||
goodsName?: string // 商品名称(模糊查询)
|
||||
categoryId?: number // 商品分类ID
|
||||
externalGoodsId?: number // 外部归属类型的商品ID
|
||||
corpid?: string // 企业微信id
|
||||
status?: number // 商品状态(1上架 2下架)
|
||||
autoApproval?: number // 免审批(0否 1是)
|
||||
minPrice?: number // 最低价格
|
||||
maxPrice?: number // 最高价格
|
||||
belongType?: number // 归属类型(0-借还柜 1-固资通)
|
||||
beginTime?: string // 开始时间(格式:yyyy-MM-dd)
|
||||
endTime?: string // 结束时间(格式:yyyy-MM-dd)
|
||||
}
|
||||
|
||||
/** 获取商品列表 */
|
||||
/**
|
||||
* 获取商品列表
|
||||
* 分页查询商品列表,支持多条件筛选和排序
|
||||
* @param query 查询参数(包含分页、筛选条件等)
|
||||
* @returns Promise<ResponseData<PageDTO<ShopGoodsDTO>>>
|
||||
*/
|
||||
export function getGoodsList(query: SearchShopGoodsQuery) {
|
||||
return request<ResponseData<PageDTO<ShopGoodsDTO>>>({
|
||||
url: "manage/goods/list",
|
||||
|
|
@ -40,17 +56,27 @@ export function getGoodsList(query: SearchShopGoodsQuery) {
|
|||
})
|
||||
}
|
||||
|
||||
/** 新增商品 */
|
||||
/**
|
||||
* 新增商品
|
||||
* 创建新商品
|
||||
* @param data 商品信息
|
||||
* @returns Promise<ResponseData<void>>
|
||||
*/
|
||||
export function addGoods(data: {
|
||||
goodsName: string
|
||||
categoryId: number
|
||||
price: number
|
||||
stock: number
|
||||
status: number
|
||||
autoApproval: number
|
||||
coverImg: string
|
||||
goodsDetail?: string
|
||||
usageInstruction?: string
|
||||
goodsName: string // 商品名称(必填)
|
||||
categoryId?: number // 商品分类ID
|
||||
externalGoodsId?: number // 外部归属类型的商品ID
|
||||
corpid?: string // 企业微信id
|
||||
monthlyPurchaseLimit?: number // 每人每月限购数量
|
||||
price: number // 销售价格(必填)
|
||||
stock?: number // 库存数量
|
||||
status?: number // 商品状态(1上架 2下架)
|
||||
autoApproval?: number // 免审批(0否 1是)
|
||||
coverImg?: string // 封面图URL
|
||||
goodsDetail?: string // 商品详情(支持2000汉字+10个图片链接)
|
||||
remark?: string // 备注
|
||||
usageInstruction?: string // 商品使用说明
|
||||
belongType?: number // 归属类型(0-借还柜 1-固资通)
|
||||
}) {
|
||||
return request<ResponseData<void>>({
|
||||
url: "manage/goods",
|
||||
|
|
@ -59,7 +85,12 @@ export function addGoods(data: {
|
|||
})
|
||||
}
|
||||
|
||||
/** 删除商品 */
|
||||
/**
|
||||
* 删除商品
|
||||
* 批量删除商品
|
||||
* @param goodsIds 商品ID数组
|
||||
* @returns Promise<ResponseData<void>>
|
||||
*/
|
||||
export function deleteGoods(goodsIds: number[]) {
|
||||
return request<ResponseData<void>>({
|
||||
url: `manage/goods/${goodsIds.join(',')}`,
|
||||
|
|
@ -67,17 +98,28 @@ export function deleteGoods(goodsIds: number[]) {
|
|||
})
|
||||
}
|
||||
|
||||
/** 修改商品 */
|
||||
/**
|
||||
* 修改商品
|
||||
* 更新商品信息
|
||||
* @param goodsId 商品ID(路径参数)
|
||||
* @param data 更新数据
|
||||
* @returns Promise<ResponseData<void>>
|
||||
*/
|
||||
export function updateGoods(goodsId: number, data: {
|
||||
goodsName?: string
|
||||
categoryId?: number
|
||||
price?: number
|
||||
stock?: number
|
||||
status?: number
|
||||
autoApproval?: number
|
||||
coverImg?: string
|
||||
goodsDetail?: string
|
||||
usageInstruction?: string
|
||||
goodsName?: string // 商品名称
|
||||
categoryId?: number // 商品分类ID
|
||||
externalGoodsId?: number // 外部归属类型的商品ID
|
||||
corpid?: string // 企业微信id
|
||||
monthlyPurchaseLimit?: number // 每人每月限购数量
|
||||
price?: number // 销售价格
|
||||
stock?: number // 库存数量
|
||||
status?: number // 商品状态(1上架 2下架)
|
||||
autoApproval?: number // 免审批(0否 1是)
|
||||
coverImg?: string // 封面图URL
|
||||
goodsDetail?: string // 商品详情(支持2000汉字+10个图片链接)
|
||||
remark?: string // 备注
|
||||
usageInstruction?: string // 商品使用说明
|
||||
belongType?: number // 归属类型(0-借还柜 1-固资通)
|
||||
}) {
|
||||
return request<ResponseData<void>>({
|
||||
url: `manage/goods/${goodsId}`,
|
||||
|
|
@ -86,7 +128,12 @@ export function updateGoods(goodsId: number, data: {
|
|||
})
|
||||
}
|
||||
|
||||
/** 获取单个商品信息 */
|
||||
/**
|
||||
* 获取单个商品信息
|
||||
* 根据商品ID获取商品的详细信息
|
||||
* @param goodsId 商品ID(查询参数)
|
||||
* @returns Promise<ResponseData<ShopGoodsDTO>>
|
||||
*/
|
||||
export function getGoodsInfo(goodsId: number) {
|
||||
return request<ResponseData<ShopGoodsDTO>>({
|
||||
url: "manage/goods/getGoodsInfo",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ const wxStore = useWxStoreOutside();
|
|||
const tabbarItemList = computed(() => {
|
||||
if (wxStore.isCabinetAdmin) {
|
||||
return [{
|
||||
title: '商品管理',
|
||||
icon: 'home-o',
|
||||
path: '/manage/goods'
|
||||
},
|
||||
{
|
||||
title: '柜机管理',
|
||||
icon: 'manager-o',
|
||||
path: '/cabinet'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,387 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showSuccessToast, showFailToast, showConfirmDialog } from 'vant'
|
||||
import Compressor from 'compressorjs'
|
||||
import axios from "axios"
|
||||
import { UploaderFileListItem } from 'vant'
|
||||
import {
|
||||
addGoods,
|
||||
updateGoods,
|
||||
getGoodsInfo,
|
||||
type ShopGoodsDTO
|
||||
} from '@/common/apis/manage/goods'
|
||||
import { useWxStoreOutside } from '@/pinia/stores/wx'
|
||||
|
||||
const { VITE_APP_BASE_API } = import.meta.env
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const wxStore = useWxStoreOutside()
|
||||
|
||||
// 判断是新增还是编辑模式
|
||||
const isEditMode = ref(false)
|
||||
const goodsId = ref<number | undefined>(undefined)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
goodsName: '',
|
||||
categoryId: undefined as number | undefined,
|
||||
externalGoodsId: undefined as number | undefined,
|
||||
corpid: wxStore.corpid || '',
|
||||
monthlyPurchaseLimit: undefined as number | undefined,
|
||||
price: undefined as number | undefined,
|
||||
stock: undefined as number | undefined,
|
||||
status: 1, // 默认上架
|
||||
autoApproval: 0, // 默认不免审批
|
||||
coverImg: '',
|
||||
goodsDetail: '',
|
||||
remark: '',
|
||||
usageInstruction: '',
|
||||
belongType: undefined as number | undefined
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
|
||||
// 图片上传状态
|
||||
const fileList = ref<UploaderFileListItem[]>([])
|
||||
const uploading = ref(false)
|
||||
|
||||
// 监听文件列表变化,更新封面图URL
|
||||
watch(fileList, (newList) => {
|
||||
if (newList.length === 0) {
|
||||
formData.coverImg = ''
|
||||
} else {
|
||||
// 如果列表中有图片且URL有效,更新封面图URL
|
||||
const item = newList[0]
|
||||
if (item.url) {
|
||||
formData.coverImg = item.url
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 编辑模式下加载商品数据
|
||||
const loadGoodsData = async (id: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await getGoodsInfo(id)
|
||||
const goods = res.data
|
||||
|
||||
// 填充表单数据
|
||||
Object.assign(formData, {
|
||||
goodsName: goods.goodsName || '',
|
||||
categoryId: goods.categoryId,
|
||||
externalGoodsId: goods.externalGoodsId,
|
||||
corpid: goods.corpid || wxStore.corpid || '',
|
||||
monthlyPurchaseLimit: goods.monthlyPurchaseLimit,
|
||||
price: goods.price,
|
||||
stock: goods.stock,
|
||||
status: goods.status || 1,
|
||||
autoApproval: goods.autoApproval || 0,
|
||||
coverImg: goods.coverImg || '',
|
||||
goodsDetail: goods.goodsDetail || '',
|
||||
remark: goods.remark || '',
|
||||
usageInstruction: goods.usageInstruction || '',
|
||||
belongType: goods.belongType
|
||||
})
|
||||
|
||||
// 如果有封面图URL,添加到文件列表
|
||||
if (goods.coverImg) {
|
||||
fileList.value = [{
|
||||
url: goods.coverImg,
|
||||
status: 'done',
|
||||
message: '已上传'
|
||||
}]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商品数据失败:', error)
|
||||
showFailToast('加载商品数据失败')
|
||||
router.back()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传处理
|
||||
const handleFileUpload = async (items: UploaderFileListItem | UploaderFileListItem[]) => {
|
||||
const files = Array.isArray(items) ? items : [items]
|
||||
uploading.value = true
|
||||
try {
|
||||
const uploadPromises = files.map(async (item) => {
|
||||
item.status = 'uploading'
|
||||
item.message = '上传中...'
|
||||
const file = item.file as File
|
||||
let compressedFile = file
|
||||
try {
|
||||
compressedFile = await new Promise<File>((resolve, reject) => {
|
||||
new Compressor(file, {
|
||||
quality: 0.8,
|
||||
maxWidth: 1280,
|
||||
maxHeight: 1280,
|
||||
success(result) {
|
||||
resolve(new File([result], file.name, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now()
|
||||
}))
|
||||
},
|
||||
error(err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('压缩失败:', error)
|
||||
}
|
||||
|
||||
const uploadFormData = new FormData()
|
||||
uploadFormData.append('file', compressedFile)
|
||||
|
||||
const { data } = await axios.post<{
|
||||
code: number
|
||||
data: { url: string }
|
||||
message?: string
|
||||
}>(VITE_APP_BASE_API + '/file/upload', uploadFormData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
|
||||
if (data.code !== 0) {
|
||||
throw new Error(data.message || '文件上传失败')
|
||||
}
|
||||
return { url: data.data.url }
|
||||
})
|
||||
|
||||
const urls = await Promise.all(uploadPromises)
|
||||
files.forEach((item, index) => {
|
||||
item.status = 'done'
|
||||
item.message = '上传成功'
|
||||
item.url = urls[index].url
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
showConfirmDialog({
|
||||
title: '上传失败',
|
||||
message: error instanceof Error ? error.message : '未知错误'
|
||||
})
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
const id = route.params.id
|
||||
if (id) {
|
||||
const idStr = Array.isArray(id) ? id[0] : id
|
||||
if (idStr) {
|
||||
isEditMode.value = true
|
||||
goodsId.value = parseInt(idStr, 10)
|
||||
if (!isNaN(goodsId.value)) {
|
||||
loadGoodsData(goodsId.value)
|
||||
} else {
|
||||
showFailToast('商品ID无效')
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
// 表单验证
|
||||
if (!formData.goodsName.trim()) {
|
||||
showFailToast('请输入商品名称')
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
if (!formData.price || formData.price <= 0) {
|
||||
showFailToast('请输入有效的价格')
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (isEditMode.value && goodsId.value) {
|
||||
// 编辑商品
|
||||
await updateGoods(goodsId.value, {
|
||||
goodsName: formData.goodsName,
|
||||
price: formData.price,
|
||||
stock: formData.stock,
|
||||
status: formData.status,
|
||||
autoApproval: formData.autoApproval,
|
||||
coverImg: formData.coverImg || undefined,
|
||||
usageInstruction: formData.usageInstruction || undefined,
|
||||
corpid: formData.corpid || undefined
|
||||
})
|
||||
showSuccessToast('更新成功')
|
||||
} else {
|
||||
// 新增商品
|
||||
await addGoods({
|
||||
goodsName: formData.goodsName,
|
||||
price: formData.price!,
|
||||
stock: formData.stock,
|
||||
status: formData.status,
|
||||
autoApproval: formData.autoApproval,
|
||||
coverImg: formData.coverImg || undefined,
|
||||
usageInstruction: formData.usageInstruction || undefined,
|
||||
corpid: formData.corpid || undefined
|
||||
})
|
||||
showSuccessToast('添加成功')
|
||||
}
|
||||
|
||||
// 返回商品列表
|
||||
router.push('/manage/goods')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
showFailToast('提交失败,请重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 返回确认
|
||||
const handleBack = () => {
|
||||
showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定放弃编辑并返回吗?'
|
||||
}).then(() => {
|
||||
router.back()
|
||||
}).catch(() => {
|
||||
// 取消
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="goods-add-edit-page">
|
||||
<!-- 导航栏 -->
|
||||
<van-nav-bar
|
||||
:title="isEditMode ? '编辑商品' : '新增商品'"
|
||||
left-arrow
|
||||
@click-left="handleBack"
|
||||
>
|
||||
<template #right>
|
||||
<van-button
|
||||
v-if="!loading"
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="submitForm"
|
||||
>
|
||||
{{ submitting ? '提交中...' : '提交' }}
|
||||
</van-button>
|
||||
</template>
|
||||
</van-nav-bar>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<van-loading v-if="loading" size="24px" vertical>加载中...</van-loading>
|
||||
|
||||
<!-- 表单 -->
|
||||
<van-form v-else @submit="submitForm">
|
||||
<van-cell-group inset>
|
||||
<!-- 商品名称 -->
|
||||
<van-field
|
||||
v-model="formData.goodsName"
|
||||
label="商品名称"
|
||||
placeholder="请输入商品名称"
|
||||
:rules="[{ required: true, message: '请输入商品名称' }]"
|
||||
/>
|
||||
|
||||
<!-- 价格 -->
|
||||
<van-field
|
||||
v-model.number="formData.price"
|
||||
label="价格"
|
||||
type="number"
|
||||
placeholder="请输入价格"
|
||||
:rules="[{ required: true, message: '请输入价格' }]"
|
||||
>
|
||||
<template #extra>
|
||||
<span v-if="formData.price !== undefined">¥{{ formData.price.toFixed(2) }}</span>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<!-- 库存 -->
|
||||
<van-field
|
||||
v-model.number="formData.stock"
|
||||
label="库存"
|
||||
type="number"
|
||||
placeholder="请输入库存数量"
|
||||
/>
|
||||
|
||||
<!-- 状态 -->
|
||||
<van-field name="status" label="状态">
|
||||
<template #input>
|
||||
<van-radio-group v-model="formData.status" direction="horizontal">
|
||||
<van-radio :name="1">上架</van-radio>
|
||||
<van-radio :name="2">下架</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<!-- 免审批 -->
|
||||
<van-field name="autoApproval" label="免审批">
|
||||
<template #input>
|
||||
<van-radio-group v-model="formData.autoApproval" direction="horizontal">
|
||||
<van-radio :name="0">否</van-radio>
|
||||
<van-radio :name="1">是</van-radio>
|
||||
</van-radio-group>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 封面图 -->
|
||||
<van-field name="coverImg" label="封面图">
|
||||
<template #input>
|
||||
<van-uploader
|
||||
v-model="fileList"
|
||||
:max-count="1"
|
||||
:after-read="handleFileUpload"
|
||||
:disabled="uploading"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<van-field
|
||||
v-model="formData.usageInstruction"
|
||||
label="使用说明"
|
||||
type="textarea"
|
||||
placeholder="请输入商品使用说明"
|
||||
rows="2"
|
||||
autosize
|
||||
/>
|
||||
|
||||
|
||||
</van-cell-group>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<div style="margin: 16px">
|
||||
<van-button round block type="primary" native-type="submit" :loading="submitting">
|
||||
{{ submitting ? '提交中...' : '提交' }}
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.goods-add-edit-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
.van-form {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.van-field__label {
|
||||
min-width: 90px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showLoadingToast, closeToast, showFailToast } from 'vant'
|
||||
import { getGoodsInfo, type ShopGoodsDTO } from '@/common/apis/manage/goods'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const goodsId = ref<number | null>(null)
|
||||
const goods = ref<ShopGoodsDTO>({})
|
||||
const loading = ref(false)
|
||||
|
||||
// 获取商品详情
|
||||
const fetchGoodsDetail = async (id: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const toast = showLoadingToast({ message: '加载中...', duration: 0 })
|
||||
const res = await getGoodsInfo(id)
|
||||
goods.value = res.data
|
||||
closeToast()
|
||||
} catch (error) {
|
||||
console.error('加载商品详情失败:', error)
|
||||
showFailToast('加载失败')
|
||||
router.back()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到编辑页面
|
||||
const navigateToEdit = () => {
|
||||
if (goods.value.goodsId) {
|
||||
router.push(`/manage/goods/edit/${goods.value.goodsId}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
const id = route.params.id
|
||||
if (id) {
|
||||
const idStr = Array.isArray(id) ? id[0] : id
|
||||
if (idStr) {
|
||||
goodsId.value = parseInt(idStr, 10)
|
||||
if (!isNaN(goodsId.value)) {
|
||||
fetchGoodsDetail(goodsId.value)
|
||||
} else {
|
||||
showFailToast('商品ID无效')
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="goods-detail-page">
|
||||
<!-- 导航栏 -->
|
||||
<van-nav-bar
|
||||
title="商品详情"
|
||||
left-arrow
|
||||
@click-left="router.back"
|
||||
/>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<van-loading v-if="loading" size="24px" vertical>加载中...</van-loading>
|
||||
|
||||
<!-- 商品详情内容 -->
|
||||
<div v-else class="goods-content">
|
||||
<!-- 商品图片 -->
|
||||
<div class="goods-image-section">
|
||||
<van-image
|
||||
v-if="goods.coverImg"
|
||||
:src="goods.coverImg"
|
||||
class="goods-main-image"
|
||||
fit="cover"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<van-icon name="photo-fail" size="40" />
|
||||
<div>图片加载失败</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-image>
|
||||
<div v-else class="no-image">
|
||||
<van-icon name="photo" size="60" />
|
||||
<div>暂无图片</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品基本信息 -->
|
||||
<div class="goods-basic-info">
|
||||
<h2 class="goods-name">{{ goods.goodsName || '未知商品' }}</h2>
|
||||
<div class="goods-price">¥{{ goods.price?.toFixed(2) || '0.00' }}</div>
|
||||
|
||||
<div class="goods-meta">
|
||||
<van-tag v-if="goods.status === 1" type="success">上架</van-tag>
|
||||
<van-tag v-else-if="goods.status === 2" type="danger">下架</van-tag>
|
||||
<van-tag v-if="goods.stock! > 0" type="primary">库存: {{ goods.stock }}</van-tag>
|
||||
<van-tag v-else type="warning">已售罄</van-tag>
|
||||
<van-tag v-if="goods.autoApproval === 1" type="success">免审批</van-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 商品详情 -->
|
||||
<div class="goods-detail-section">
|
||||
<h3 class="section-title">商品详情</h3>
|
||||
<div class="detail-content" v-html="goods.goodsDetail || '暂无商品详情'"></div>
|
||||
</div>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<div v-if="goods.usageInstruction" class="usage-section">
|
||||
<h3 class="section-title">使用说明</h3>
|
||||
<div class="usage-content">{{ goods.usageInstruction }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 备注 -->
|
||||
<div v-if="goods.remark" class="remark-section">
|
||||
<h3 class="section-title">备注</h3>
|
||||
<div class="remark-content">{{ goods.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 固定在右下角的圆形编辑按钮 -->
|
||||
<div class="floating-edit-button">
|
||||
<van-button
|
||||
round
|
||||
type="primary"
|
||||
size="large"
|
||||
icon="edit"
|
||||
@click="navigateToEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.goods-detail-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f7f8fa;
|
||||
padding-bottom: 80px; /* 为浮动按钮留出空间 */
|
||||
}
|
||||
|
||||
.goods-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 商品图片区域 */
|
||||
.goods-image-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.goods-main-image {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #969799;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
.no-image {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #969799;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
/* 商品基本信息 */
|
||||
.goods-basic-info {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.goods-price {
|
||||
font-size: 24px;
|
||||
color: #ff4444;
|
||||
font-weight: 700;
|
||||
margin: 8px 0 12px 0;
|
||||
}
|
||||
|
||||
.goods-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 详情区域 */
|
||||
.goods-detail-section,
|
||||
.usage-section,
|
||||
.remark-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.detail-content :deep(img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.usage-content,
|
||||
.remark-content {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 浮动编辑按钮 */
|
||||
.floating-edit-button {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 30px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.floating-edit-button .van-button {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.floating-edit-button .van-button .van-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, onMounted, watch, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { showConfirmDialog, showDialog, showSuccessToast } from 'vant';
|
||||
import { debounce } from 'lodash-es';
|
||||
import {
|
||||
getGoodsList,
|
||||
addGoods,
|
||||
deleteGoods,
|
||||
updateGoods,
|
||||
type ShopGoodsDTO,
|
||||
type SearchShopGoodsQuery
|
||||
} from '@/common/apis/manage/goods';
|
||||
|
|
@ -23,14 +23,13 @@ const searchParams = reactive<SearchShopGoodsQuery>({
|
|||
status: undefined
|
||||
});
|
||||
|
||||
// 弹窗控制
|
||||
const showEditDialog = ref(false);
|
||||
const currentGoods = ref<Partial<ShopGoodsDTO>>({});
|
||||
const isEditMode = ref(false);
|
||||
// 路由
|
||||
const router = useRouter();
|
||||
|
||||
// 获取商品列表
|
||||
const fetchGoodsList = async () => {
|
||||
try {
|
||||
searchParams.pageNum = 1;
|
||||
const res = await getGoodsList(searchParams);
|
||||
goodsList.value = res.data.rows;
|
||||
} catch (e) {
|
||||
|
|
@ -38,6 +37,23 @@ const fetchGoodsList = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// 防抖搜索
|
||||
const debouncedFetchGoodsList = debounce(fetchGoodsList, 500);
|
||||
|
||||
// 监听搜索条件变化
|
||||
watch(() => searchParams.goodsName, debouncedFetchGoodsList);
|
||||
|
||||
// 组件卸载时取消防抖
|
||||
onUnmounted(() => {
|
||||
debouncedFetchGoodsList.cancel();
|
||||
});
|
||||
|
||||
// 搜索按钮触发
|
||||
const handleSearch = () => {
|
||||
debouncedFetchGoodsList.cancel();
|
||||
fetchGoodsList();
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const onLoad = async () => {
|
||||
try {
|
||||
|
|
@ -68,36 +84,21 @@ const handleDelete = async (ids: number[]) => {
|
|||
await fetchGoodsList();
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
if (isEditMode.value) {
|
||||
await updateGoods(currentGoods.value.goodsId!, currentGoods.value);
|
||||
} else {
|
||||
await addGoods({
|
||||
goodsName: currentGoods.value.goodsName || '',
|
||||
categoryId: currentGoods.value.categoryId || 0,
|
||||
price: currentGoods.value.price || 0,
|
||||
stock: currentGoods.value.stock || 0,
|
||||
status: currentGoods.value.status || 1,
|
||||
autoApproval: currentGoods.value.autoApproval || 0,
|
||||
coverImg: currentGoods.value.coverImg || '',
|
||||
usageInstruction: currentGoods.value.usageInstruction
|
||||
});
|
||||
}
|
||||
showSuccessToast('操作成功');
|
||||
showEditDialog.value = false;
|
||||
await fetchGoodsList();
|
||||
} catch (e) {
|
||||
showDialog({ message: '操作失败' });
|
||||
|
||||
// 跳转到新增/编辑页面
|
||||
const navigateToEdit = (goods?: ShopGoodsDTO) => {
|
||||
if (goods?.goodsId) {
|
||||
router.push(`/manage/goods/edit/${goods.goodsId}`);
|
||||
} else {
|
||||
router.push('/manage/goods/add');
|
||||
}
|
||||
};
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEdit = (goods?: ShopGoodsDTO) => {
|
||||
currentGoods.value = goods ? { ...goods } : { status: 1 };
|
||||
isEditMode.value = !!goods;
|
||||
showEditDialog.value = true;
|
||||
// 跳转到详情页面
|
||||
const navigateToDetail = (goods: ShopGoodsDTO) => {
|
||||
if (goods?.goodsId) {
|
||||
router.push(`/manage/goods/detail/${goods.goodsId}`);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(fetchGoodsList);
|
||||
|
|
@ -107,16 +108,16 @@ onMounted(fetchGoodsList);
|
|||
<div class="goods-manage">
|
||||
<!-- 搜索栏和操作按钮 -->
|
||||
<div class="search-action-bar">
|
||||
<van-search v-model="searchParams.goodsName" placeholder="输入商品名称搜索" @search="fetchGoodsList" />
|
||||
<van-button type="primary" @click="openEdit">添加商品</van-button>
|
||||
<van-search v-model="searchParams.goodsName" placeholder="输入商品名称搜索" @search="handleSearch" />
|
||||
<van-button type="primary" @click="navigateToEdit">添加商品</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 商品表格 -->
|
||||
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
|
||||
<div class="goods-grid">
|
||||
<van-cell v-for="item in goodsList" :key="item.goodsId" class="goods-card">
|
||||
<template #icon>
|
||||
<van-image :src="item.coverImg" width="80" height="80" class="goods-image">
|
||||
<div v-for="item in goodsList" :key="item.goodsId" class="goods-card" @click="navigateToDetail(item)" style="cursor: pointer;">
|
||||
<div class="goods-image-container">
|
||||
<van-image :src="item.coverImg" class="goods-image">
|
||||
<div v-if="item.stock === 0" class="sold-out-overlay">
|
||||
<span class="sold-out-text">已售罄</span>
|
||||
</div>
|
||||
|
|
@ -126,109 +127,141 @@ onMounted(fetchGoodsList);
|
|||
</div>
|
||||
</template>
|
||||
</van-image>
|
||||
</template>
|
||||
</div>
|
||||
<div class="goods-info">
|
||||
<div class="goods-name van-ellipsis">{{ item.goodsName }}</div>
|
||||
<div class="goods-price">¥{{ item.price?.toFixed(2) }}</div>
|
||||
<div class="action-row">
|
||||
<div class="goods-footer">
|
||||
<span v-if="item.stock! > 0" class="stock-count">库存: {{ item.stock }}</span>
|
||||
<div class="goods-actions">
|
||||
<van-button size="mini" @click="openEdit(item)">编辑</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-cell>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<van-dialog v-model:show="showEditDialog" :title="isEditMode ? '编辑商品' : '新增商品'">
|
||||
<van-form @submit="submitForm">
|
||||
<van-cell-group inset>
|
||||
<van-field v-model="currentGoods.goodsName" label="商品名称" placeholder="请输入"
|
||||
:rules="[{ required: true }]" />
|
||||
<van-field v-model="currentGoods.price" label="价格" type="number" placeholder="请输入" />
|
||||
<van-field v-model="currentGoods.stock" label="库存" type="number" placeholder="请输入" />
|
||||
</van-cell-group>
|
||||
<div style="padding: 16px">
|
||||
<van-button block type="primary" native-type="submit">提交</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</van-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.goods-manage {
|
||||
padding: 4px;
|
||||
padding: 12px;
|
||||
background-color: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.search-action-bar {
|
||||
margin: 0;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 12px;
|
||||
background: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.search-action-bar .van-search {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-action-bar .van-search .van-search__content {
|
||||
border-radius: 8px;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
.goods-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 4px;
|
||||
margin: 4px 0;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
margin-bottom: 10px;
|
||||
padding: min(2.667vw, 20px) 0;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.goods-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.goods-image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 100%; /* 1:1 正方形比例 */
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
margin-right: 12px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.goods-image .van-image__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
padding: 12px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.goods-price {
|
||||
font-size: 16px;
|
||||
color: #e95d5d;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
font-size: 18px;
|
||||
color: #ff4444;
|
||||
font-weight: 700;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
.goods-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.stock-count {
|
||||
font-size: 11px;
|
||||
color: #bbbbbb;
|
||||
margin-right: 8px;
|
||||
.goods-actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
line-height: 1;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stock-count {
|
||||
font-size: 12px;
|
||||
color: #969799;
|
||||
background-color: #f7f8fa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.sold-out-overlay {
|
||||
|
|
@ -237,25 +270,37 @@ onMounted(fetchGoodsList);
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sold-out-text {
|
||||
color: #999;
|
||||
color: #ff4444;
|
||||
font-size: 14px;
|
||||
transform: rotate(-15deg);
|
||||
border: 1px solid #eee;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
border: 2px solid #ff4444;
|
||||
transform: rotate(-5deg);
|
||||
box-shadow: 0 2px 8px rgba(255, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.custom-error {
|
||||
color: #999;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #969799;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -181,7 +181,7 @@ export const routes: RouteRecordRaw[] = [
|
|||
}
|
||||
}
|
||||
},
|
||||
/* {
|
||||
{
|
||||
path: '/manage/goods',
|
||||
component: () => import('@/pages/manage/goods/goodsList.vue'),
|
||||
name: "ManageGoods",
|
||||
|
|
@ -190,16 +190,70 @@ export const routes: RouteRecordRaw[] = [
|
|||
keepAlive: false,
|
||||
layout: {
|
||||
navBar: {
|
||||
showNavBar: true,
|
||||
showLeftArrow: true
|
||||
showNavBar: false,
|
||||
showLeftArrow: false
|
||||
},
|
||||
tabbar: {
|
||||
showTabbar: false,
|
||||
showTabbar: true,
|
||||
icon: "home-o"
|
||||
}
|
||||
}
|
||||
}
|
||||
}, */
|
||||
},
|
||||
{
|
||||
path: '/manage/goods/add',
|
||||
component: () => import('@/pages/manage/goods/goodsAddEdit.vue'),
|
||||
name: "ManageGoodsAdd",
|
||||
meta: {
|
||||
title: '新增商品',
|
||||
keepAlive: false,
|
||||
layout: {
|
||||
navBar: {
|
||||
showNavBar: true,
|
||||
showLeftArrow: true
|
||||
},
|
||||
tabbar: {
|
||||
showTabbar: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/manage/goods/edit/:id',
|
||||
component: () => import('@/pages/manage/goods/goodsAddEdit.vue'),
|
||||
name: "ManageGoodsEdit",
|
||||
meta: {
|
||||
title: '编辑商品',
|
||||
keepAlive: false,
|
||||
layout: {
|
||||
navBar: {
|
||||
showNavBar: true,
|
||||
showLeftArrow: true
|
||||
},
|
||||
tabbar: {
|
||||
showTabbar: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/manage/goods/detail/:id',
|
||||
component: () => import('@/pages/manage/goods/goodsDetail.vue'),
|
||||
name: "ManageGoodsDetail",
|
||||
meta: {
|
||||
title: '商品详情',
|
||||
keepAlive: false,
|
||||
layout: {
|
||||
navBar: {
|
||||
showNavBar: true,
|
||||
showLeftArrow: true
|
||||
},
|
||||
tabbar: {
|
||||
showTabbar: false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/productList",
|
||||
component: () => import("@/pages/product/ProductList.vue"),
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ declare module 'vue' {
|
|||
VanPicker: typeof import('vant/es')['Picker']
|
||||
VanPopup: typeof import('vant/es')['Popup']
|
||||
VanPullRefresh: typeof import('vant/es')['PullRefresh']
|
||||
VanRadio: typeof import('vant/es')['Radio']
|
||||
VanRadioGroup: typeof import('vant/es')['RadioGroup']
|
||||
VanRow: typeof import('vant/es')['Row']
|
||||
VanSearch: typeof import('vant/es')['Search']
|
||||
VanSidebar: typeof import('vant/es')['Sidebar']
|
||||
|
|
|
|||
Loading…
Reference in New Issue