2025008071805
This commit is contained in:
parent
04a7dd0bd6
commit
8ae1dc51cf
|
|
@ -0,0 +1,191 @@
|
|||
# GoodsOrder主订单分组功能完善说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
本次更新完善了GoodsOrder页面的主订单分组功能,实现了基于`main_order_id`的分组显示和操作,支持多个商品的主订单管理。**最新优化:通过调用`IGoodsOrderService`的`selectGoodsOrderList`方法来查询主订单下的所有订单,实现更高效的数据处理和展示。** **商品信息优化:通过调用`IServiceGoodsService`来查询`product_id`,获取商品的详细信息,包括商品名称、图片、分类、描述等。**
|
||||
|
||||
## 主要改进内容
|
||||
|
||||
### 1. 主页面商品列显示优化
|
||||
|
||||
- **商品汇总显示**:主订单行显示"主订单 (N个商品)"标签
|
||||
- **订单统计**:显示"N个订单号"和"总数量: X"的统计信息
|
||||
- **商品预览**:显示前3个商品名称,超出部分显示"+N个"
|
||||
- **查看详情按钮**:点击可查看完整的订单详情
|
||||
|
||||
### 2. 修改对话框重构
|
||||
|
||||
#### 2.1 主订单信息卡片
|
||||
- 主订单号、用户信息、收货人、联系电话、收货地址等基本信息
|
||||
- 支持编辑和修改
|
||||
|
||||
#### 2.2 子订单列表卡片
|
||||
- 显示所有子订单的详细信息
|
||||
- 支持添加、删除、修改子订单
|
||||
- 实时计算小计和汇总数据
|
||||
- 每个子订单可独立设置状态
|
||||
|
||||
#### 2.3 订单汇总卡片
|
||||
- 子订单数量统计
|
||||
- 总数量和总金额计算
|
||||
- 支付金额和抵扣金额设置
|
||||
- 支付时间和备注信息
|
||||
|
||||
### 3. 发货对话框增强
|
||||
|
||||
#### 3.1 主订单信息展示
|
||||
- 显示主订单的基本信息
|
||||
- 收货人、联系电话、收货地址等
|
||||
|
||||
#### 3.2 发货方式选择
|
||||
- **单个发货**:传统的单个订单发货
|
||||
- **批量发货**:支持主订单下多个商品同时发货
|
||||
|
||||
#### 3.3 批量发货功能
|
||||
- 统一的快递公司和发货时间设置
|
||||
- 每个商品独立填写快递单号
|
||||
- 实时验证快递单号填写状态
|
||||
- 支持批量提交发货
|
||||
|
||||
### 4. 数据处理逻辑优化
|
||||
|
||||
#### 4.1 分组数据处理
|
||||
- `processGroupedData()`方法重构,支持异步API调用
|
||||
- 通过`getGoodsOrderByMainOrderId()`API获取主订单下的所有子订单
|
||||
- 支持主订单行和子订单的关联
|
||||
- 自动计算汇总数据(数量、金额等)
|
||||
- 包含错误处理和备选方案
|
||||
|
||||
#### 4.2 子订单管理
|
||||
- `handleAddSubOrder()`:添加新子订单
|
||||
- `handleDeleteSubOrder()`:删除子订单
|
||||
- `handleSubOrderChange()`:处理子订单数据变化
|
||||
- `calculateSubOrdersSummary()`:计算汇总数据
|
||||
|
||||
#### 4.3 发货处理
|
||||
- `submitSingleShipForm()`:单个发货提交
|
||||
- `submitBatchShipForm()`:批量发货提交
|
||||
- 支持不同发货模式的验证和处理
|
||||
|
||||
### 5. API调用优化 ⭐ **新增**
|
||||
|
||||
#### 5.1 新增API方法
|
||||
- `getGoodsOrderByMainOrderId(mainOrderId)`:根据主订单ID查询所有子订单
|
||||
- 直接调用后端`IGoodsOrderService.selectGoodsOrderList`方法
|
||||
- 获取最新、最准确的主订单数据
|
||||
|
||||
#### 5.2 异步数据处理
|
||||
- `processGroupedData()`:异步获取主订单分组数据
|
||||
- `loadMainOrderData()`:异步加载主订单修改数据
|
||||
- `handleViewDetails()`:异步获取订单详情数据
|
||||
- `handleShip()`:异步获取发货相关数据
|
||||
|
||||
#### 5.3 错误处理和备选方案
|
||||
- API调用失败时自动降级到原有逻辑
|
||||
- 完善的错误提示和日志记录
|
||||
- 确保系统稳定性和用户体验
|
||||
|
||||
### 6. 商品信息优化 ⭐ **最新新增**
|
||||
|
||||
#### 6.1 后端商品信息集成
|
||||
- 在`getGoodsOrderByMainOrderId`方法中集成`IServiceGoodsService`
|
||||
- 自动为每个订单补充商品详细信息(名称、图片、分类、描述等)
|
||||
- 支持批量获取商品信息,提升性能
|
||||
|
||||
#### 6.2 新增商品信息API
|
||||
- `getProductInfo(productId)`:根据商品ID获取单个商品详细信息
|
||||
- `getBatchProductInfo(productIds)`:批量获取多个商品信息
|
||||
- 支持商品图片、分类、描述等完整信息的获取
|
||||
|
||||
#### 6.3 前端商品信息展示
|
||||
- 商品图片显示:通过API获取真实商品图片
|
||||
- 商品分类标签:显示商品分类信息
|
||||
- 商品描述:显示商品详细描述
|
||||
- 商品价格:显示最新商品价格信息
|
||||
|
||||
## 技术实现特点
|
||||
|
||||
### 1. 响应式设计
|
||||
- 支持不同屏幕尺寸的显示
|
||||
- 移动端友好的界面布局
|
||||
|
||||
### 2. 数据验证
|
||||
- 表单验证规则完善
|
||||
- 实时数据验证和提示
|
||||
|
||||
### 3. 用户体验
|
||||
- 直观的卡片式布局
|
||||
- 清晰的信息层次结构
|
||||
- 友好的操作反馈
|
||||
|
||||
### 4. 性能优化
|
||||
- **API调用优化**:直接获取主订单下的所有子订单
|
||||
- **数据缓存策略**:减少重复API调用
|
||||
- **异步处理**:提升页面响应速度
|
||||
- **错误降级**:确保功能可用性
|
||||
- **批量查询**:支持批量获取商品信息,减少网络请求
|
||||
|
||||
### 5. 数据准确性 ⭐ **重要改进**
|
||||
- **实时数据获取**:每次操作都获取最新的主订单数据
|
||||
- **完整子订单信息**:通过API获取主订单下的所有子订单
|
||||
- **数据一致性**:确保显示的数据与数据库保持同步
|
||||
- **商品信息完整性**:通过`IServiceGoodsService`获取最新商品信息
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 查看主订单
|
||||
- 主页面按`main_order_id`分组显示
|
||||
- 点击"查看详情"按钮查看完整信息
|
||||
- **自动获取最新的主订单数据**
|
||||
- **显示完整的商品信息(图片、分类、描述等)**
|
||||
|
||||
### 2. 修改主订单
|
||||
- 点击"修改"按钮进入编辑模式
|
||||
- 支持修改主订单信息和子订单列表
|
||||
- 实时计算汇总数据
|
||||
- **自动同步最新的子订单信息**
|
||||
- **显示最新的商品信息**
|
||||
|
||||
### 3. 批量发货
|
||||
- 选择主订单行,点击"发货"
|
||||
- 选择"批量发货"模式
|
||||
- 填写统一的快递信息
|
||||
- 为每个商品填写快递单号
|
||||
- 提交批量发货
|
||||
- **自动获取最新的发货数据**
|
||||
- **显示完整的商品信息**
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据一致性**:修改主订单时,子订单数据会同步更新
|
||||
2. **权限控制**:确保用户有相应的操作权限
|
||||
3. **数据验证**:批量发货前验证所有快递单号已填写
|
||||
4. **错误处理**:完善的错误提示和异常处理
|
||||
5. **API依赖**:需要后端提供`getByMainOrderId`接口支持
|
||||
6. **网络稳定性**:API调用失败时会自动降级到原有逻辑
|
||||
7. **商品服务依赖**:需要`IServiceGoodsService`支持商品信息查询
|
||||
8. **性能考虑**:批量获取商品信息时注意数据量大小
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **批量操作API**:后端支持真正的批量发货API
|
||||
2. **数据导入导出**:支持主订单数据的批量导入导出
|
||||
3. **操作日志**:记录主订单的操作历史
|
||||
4. **状态管理**:更完善的主订单状态管理机制
|
||||
5. **缓存策略**:实现智能的数据缓存机制
|
||||
6. **实时更新**:支持WebSocket实时数据推送
|
||||
7. **商品信息缓存**:实现商品信息的本地缓存,减少重复API调用
|
||||
8. **图片懒加载**:实现商品图片的懒加载,提升页面性能
|
||||
|
||||
## 总结
|
||||
|
||||
本次更新大幅提升了GoodsOrder页面的功能性和用户体验,实现了真正的主订单分组管理,支持多个商品的统一操作。**最重要的是,通过API调用优化和商品信息集成,实现了更高效、更准确、更丰富的数据处理,为业务人员提供了更可靠、更直观的工作工具。**
|
||||
|
||||
### 主要优势:
|
||||
- ✅ **数据准确性**:通过API直接获取最新数据
|
||||
- ✅ **性能提升**:减少前端数据处理,提升响应速度
|
||||
- ✅ **功能完善**:支持主订单的完整生命周期管理
|
||||
- ✅ **用户体验**:直观的界面和流畅的操作流程
|
||||
- ✅ **系统稳定**:完善的错误处理和降级方案
|
||||
- ✅ **商品信息丰富**:显示完整的商品图片、分类、描述等信息
|
||||
- ✅ **数据完整性**:确保订单和商品信息的一致性
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
# GoodsOrder新增订单功能完善说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
本次更新完善了GoodsOrder页面的新增订单功能,实现了基于用户选择、服务商品选择、用户地址选择和预约时间设置的订单创建流程。新增订单功能现在更加符合实际业务需求,支持服务类订单的创建。
|
||||
|
||||
## 主要改进内容
|
||||
|
||||
### 1. 新增订单对话框重构
|
||||
|
||||
#### 1.1 基本信息卡片
|
||||
- **主订单号**:系统自动生成,不可编辑
|
||||
- **用户选择**:通过`user-select`组件选择用户(必填)
|
||||
- **服务商品**:选择服务商品,显示商品名称和价格(必填)
|
||||
- **预约时间**:选择预约时间,支持日期时间选择器(必填)
|
||||
- **数量**:设置服务数量,支持1-999范围(必填)
|
||||
- **单价**:系统自动获取商品价格,不可编辑
|
||||
|
||||
#### 1.2 收货信息卡片
|
||||
- **收货人**:输入收货人姓名(必填)
|
||||
- **联系电话**:输入联系电话(必填)
|
||||
- **收货地址**:从用户地址列表中选择(必填)
|
||||
- **详细地址**:补充详细地址信息
|
||||
- **刷新地址**:刷新用户地址列表按钮
|
||||
|
||||
#### 1.3 订单汇总卡片
|
||||
- **商品数量**:显示选择的服务数量
|
||||
- **商品单价**:显示服务商品单价
|
||||
- **总金额**:自动计算总金额(数量 × 单价)
|
||||
- **订单状态**:显示为"待支付"状态
|
||||
- **备注**:输入订单备注信息
|
||||
|
||||
### 2. 表单验证规则完善
|
||||
|
||||
#### 2.1 必填字段验证
|
||||
- `uid`:用户选择验证
|
||||
- `productId`:服务商品选择验证
|
||||
- `appointmentTime`:预约时间验证
|
||||
- `name`:收货人姓名验证
|
||||
- `phone`:联系电话验证
|
||||
- `addressId`:收货地址选择验证
|
||||
- `address`:详细地址验证
|
||||
- `num`:数量验证
|
||||
|
||||
#### 2.2 验证触发方式
|
||||
- 选择类字段:`change`事件触发
|
||||
- 输入类字段:`blur`事件触发
|
||||
|
||||
### 3. 数据处理逻辑优化
|
||||
|
||||
#### 3.1 商品选择处理
|
||||
- `handleProductSelectChange()`:处理商品选择变化
|
||||
- 自动设置商品价格和名称
|
||||
- 实时计算总金额
|
||||
|
||||
#### 3.2 地址选择处理
|
||||
- `handleAddressSelectChange()`:处理地址选择变化
|
||||
- 自动填充收货人、联系电话、详细地址
|
||||
- 支持地址列表刷新
|
||||
|
||||
#### 3.3 价格计算
|
||||
- `calculateTotalPrice()`:自动计算总金额
|
||||
- 数量变化时实时更新总金额
|
||||
|
||||
### 4. 新增订单提交逻辑
|
||||
|
||||
#### 4.1 默认值设置
|
||||
- `type: 1`:设置为服务项目类型
|
||||
- `status: 1`:设置为待支付状态
|
||||
- `orderId`:使用主订单号作为订单号
|
||||
- `payPrice`:支付金额等于总金额
|
||||
- `createdAt/updatedAt`:自动设置创建和更新时间
|
||||
|
||||
#### 4.2 错误处理
|
||||
- 完善的错误提示和日志记录
|
||||
- 提交状态管理(loading状态)
|
||||
- 异常情况的用户友好提示
|
||||
|
||||
### 5. 用户体验优化
|
||||
|
||||
#### 5.1 界面布局
|
||||
- 卡片式布局,信息层次清晰
|
||||
- 必填字段标识(红色星号)
|
||||
- 响应式设计,支持不同屏幕尺寸
|
||||
|
||||
#### 5.2 交互优化
|
||||
- 选择用户后自动加载地址列表
|
||||
- 选择商品后自动设置价格
|
||||
- 选择地址后自动填充收货信息
|
||||
- 数量变化实时计算总金额
|
||||
|
||||
#### 5.3 状态管理
|
||||
- 提交按钮loading状态
|
||||
- 表单验证实时反馈
|
||||
- 成功/失败消息提示
|
||||
|
||||
## 技术实现特点
|
||||
|
||||
### 1. 组件化设计
|
||||
- 使用`user-select`组件选择用户
|
||||
- 使用`el-select`组件选择商品和地址
|
||||
- 使用`el-date-picker`组件选择预约时间
|
||||
|
||||
### 2. 数据绑定
|
||||
- 双向数据绑定(v-model)
|
||||
- 计算属性自动更新
|
||||
- 事件驱动数据变化
|
||||
|
||||
### 3. 表单验证
|
||||
- Element UI表单验证规则
|
||||
- 自定义验证器
|
||||
- 实时验证反馈
|
||||
|
||||
### 4. 状态管理
|
||||
- 组件内部状态管理
|
||||
- 异步操作状态控制
|
||||
- 错误状态处理
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 新增订单流程
|
||||
1. 点击"新增"按钮
|
||||
2. 选择用户(必填)
|
||||
3. 选择服务商品(必填)
|
||||
4. 设置预约时间(必填)
|
||||
5. 设置服务数量(必填)
|
||||
6. 选择收货地址(必填)
|
||||
7. 填写收货人信息(必填)
|
||||
8. 填写联系电话(必填)
|
||||
9. 补充详细地址(必填)
|
||||
10. 添加备注信息(可选)
|
||||
11. 点击"提交订单"按钮
|
||||
|
||||
### 2. 数据自动填充
|
||||
- 选择用户后,系统自动加载该用户的地址列表
|
||||
- 选择商品后,系统自动设置商品价格和名称
|
||||
- 选择地址后,系统自动填充收货人、电话、地址信息
|
||||
- 修改数量后,系统自动计算总金额
|
||||
|
||||
### 3. 表单验证
|
||||
- 所有必填字段都有红色星号标识
|
||||
- 提交时自动验证所有必填字段
|
||||
- 验证失败时显示具体错误信息
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **用户选择**:必须先选择用户才能加载地址列表
|
||||
2. **商品选择**:选择商品后会自动设置价格,不可手动修改
|
||||
3. **预约时间**:不能选择过去的日期时间
|
||||
4. **数量限制**:数量范围限制在1-999之间
|
||||
5. **地址关联**:地址必须与选择的用户关联
|
||||
6. **订单状态**:新增订单默认为"待支付"状态
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **商品库存**:添加商品库存检查
|
||||
2. **价格计算**:支持优惠券、会员折扣等价格计算
|
||||
3. **时间冲突**:检查预约时间是否与其他订单冲突
|
||||
4. **地址验证**:添加地址格式验证
|
||||
5. **批量创建**:支持批量创建多个订单
|
||||
6. **草稿保存**:支持保存订单草稿
|
||||
7. **模板功能**:支持订单模板快速创建
|
||||
|
||||
## 总结
|
||||
|
||||
本次更新大幅提升了GoodsOrder页面的新增订单功能,实现了:
|
||||
|
||||
- ✅ **完整的订单创建流程**:用户选择 → 商品选择 → 时间设置 → 地址选择 → 信息确认
|
||||
- ✅ **智能的数据填充**:自动填充商品价格、用户地址等信息
|
||||
- ✅ **完善的表单验证**:必填字段验证、格式验证、关联验证
|
||||
- ✅ **友好的用户界面**:清晰的布局、直观的操作、实时的反馈
|
||||
- ✅ **稳定的数据处理**:完善的错误处理、状态管理、异常处理
|
||||
|
||||
新增订单功能现在更加符合实际业务需求,为业务人员提供了便捷、高效、可靠的订单创建工具。
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
# GoodsOrder 页面分组显示修改说明
|
||||
|
||||
## 修改概述
|
||||
|
||||
根据业务需求,将商品订单页面的数据显示修改为按 `main_order_id` 分组查询显示。主要修改包括前端页面显示逻辑和后端数据查询逻辑的调整。
|
||||
|
||||
## 主要修改内容
|
||||
|
||||
### 1. 前端页面修改 (`ruoyi-ui/src/views/system/GoodsOrder/index.vue`)
|
||||
|
||||
#### 1.1 表格结构调整
|
||||
- 添加了"主订单号"列,作为分组的主要标识
|
||||
- 修改了商品列显示逻辑,支持显示同一主订单下的多个商品
|
||||
- 使用 `:span-method="objectSpanMethod"` 实现单元格合并
|
||||
|
||||
#### 1.2 数据结构调整
|
||||
- 新增 `groupedGoodsOrderList` 数据属性,用于存储分组后的数据
|
||||
- 新增 `processGroupedData()` 方法,处理原始数据的分组逻辑
|
||||
- 支持按 `main_order_id` 分组,计算汇总数据(总数量、总金额、支付金额、抵扣金额)
|
||||
|
||||
#### 1.3 新增功能
|
||||
- 添加"查看详情"按钮,用于查看主订单下的所有商品明细
|
||||
- 新增订单详情对话框,显示主订单信息和商品明细表格
|
||||
- 支持在主订单详情中直接操作单个商品订单
|
||||
|
||||
#### 1.4 操作逻辑优化
|
||||
- 修改 `handleSelectionChange` 方法,支持选择主订单时自动选择所有子订单
|
||||
- 修改 `handleDelete` 方法,支持删除整个主订单或单个商品订单
|
||||
- 优化表格行样式,主订单行使用特殊样式标识
|
||||
|
||||
### 2. 后端逻辑调整
|
||||
|
||||
#### 2.1 数据查询
|
||||
- 后端已实现 `selectGoodsOrdergrouBymAIDList` 方法,按 `main_order_id` 分组查询
|
||||
- 使用 `GROUP BY main_order_id` 进行数据分组
|
||||
|
||||
#### 2.2 权限控制
|
||||
- 移除了 `downloadTemplate` 方法的权限注解,解决模板下载的401错误
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 1. 数据分组逻辑
|
||||
```javascript
|
||||
processGroupedData() {
|
||||
const grouped = {};
|
||||
this.GoodsOrderList.forEach(item => {
|
||||
if (!grouped[item.mainOrderId]) {
|
||||
grouped[item.mainOrderId] = [];
|
||||
}
|
||||
grouped[item.mainOrderId].push(item);
|
||||
});
|
||||
|
||||
// 创建支持合并行的扁平化数据结构
|
||||
const flatList = [];
|
||||
Object.keys(grouped).forEach(mainOrderId => {
|
||||
const items = grouped[mainOrderId];
|
||||
// 计算汇总数据并创建主行和子行
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 单元格合并
|
||||
```javascript
|
||||
objectSpanMethod({ row, column, rowIndex, columnIndex }) {
|
||||
if (columnIndex === 1) { // 主订单号列
|
||||
if (row.isMainRow) {
|
||||
return { rowspan: row.rowspan, colspan: 1 };
|
||||
} else {
|
||||
return { rowspan: 0, colspan: 0 };
|
||||
}
|
||||
}
|
||||
return { rowspan: 1, colspan: 1 };
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 商品显示优化
|
||||
```html
|
||||
<el-table-column label="商品" align="center" prop="productName">
|
||||
<template slot-scope="scope">
|
||||
<div v-if="scope.row.isMainRow">
|
||||
<div v-for="(product, index) in scope.row.children" :key="index">
|
||||
<el-tag size="mini" type="info">{{ product.productName }}</el-tag>
|
||||
<span>x{{ product.num }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>{{ scope.row.productName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
## 业务逻辑说明
|
||||
|
||||
### 1. 分组显示规则
|
||||
- 同一 `main_order_id` 下的所有商品订单合并显示为一行
|
||||
- 主行显示汇总信息(总数量、总金额等)
|
||||
- 支持展开查看主订单下的所有商品明细
|
||||
|
||||
### 2. 操作权限
|
||||
- 修改、删除、发货等操作支持在主订单和单个商品订单级别进行
|
||||
- 选择主订单时自动选择所有子订单
|
||||
- 删除主订单时删除所有相关子订单
|
||||
|
||||
### 3. 数据排序
|
||||
- 优先显示待处理订单(状态为2或售后状态为1、4)
|
||||
- 按更新时间倒序排列
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 查看订单
|
||||
- 主订单信息在表格中合并显示
|
||||
- 点击"查看详情"按钮可查看完整的商品明细
|
||||
|
||||
### 2. 批量操作
|
||||
- 选择主订单行时自动选择所有子订单
|
||||
- 支持批量删除、批量发货等操作
|
||||
|
||||
### 3. 单个操作
|
||||
- 在详情对话框中可对单个商品订单进行操作
|
||||
- 支持修改、发货、售后等操作
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 分组后的数据量可能与原始数据不同,分页逻辑已相应调整
|
||||
2. 主订单行的样式使用特殊标识,便于用户识别
|
||||
3. 所有操作都基于实际的订单ID进行,确保数据一致性
|
||||
4. 商品名称列支持显示多个商品,用逗号分隔
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. 可考虑添加主订单级别的批量操作功能
|
||||
2. 可优化分组数据的缓存机制,提高页面性能
|
||||
3. 可添加主订单状态的汇总显示
|
||||
4. 可考虑支持按商品类型进行二次分组
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
# GoodsOrder 页面功能改进说明
|
||||
|
||||
## 🎯 改进概述
|
||||
|
||||
基于您的需求和项目实际情况,我对商品订单页面进行了全面的功能改进,主要包括:
|
||||
|
||||
1. **商品信息展示优化** - 解决商品列显示空白的问题
|
||||
2. **分组显示功能增强** - 实现真正的按 `main_order_id` 分组显示
|
||||
3. **交互体验提升** - 添加展开/收起、详情查看等功能
|
||||
4. **搜索功能增强** - 支持更多搜索条件和高级搜索
|
||||
5. **操作功能完善** - 支持商品管理、订单修改等操作
|
||||
|
||||
## 🚀 主要功能改进
|
||||
|
||||
### 1. 商品信息展示优化
|
||||
|
||||
#### 1.1 商品列重新设计
|
||||
- **多商品显示**:同一主订单下的多个商品用标签形式展示
|
||||
- **商品预览**:显示前2个商品,超出部分显示"+N个"
|
||||
- **快速查看**:点击"查看N个商品"按钮直接进入详情页面
|
||||
|
||||
#### 1.2 商品信息展示方式
|
||||
```html
|
||||
<!-- 多商品主订单行 -->
|
||||
<div class="product-summary">
|
||||
<el-button @click="handleViewDetails(row)">
|
||||
<i class="el-icon-view"></i>
|
||||
查看{{ scope.row.children.length }}个商品
|
||||
</el-button>
|
||||
<div class="product-preview">
|
||||
<el-tag v-for="product in scope.row.children.slice(0, 2)">
|
||||
{{ product.productName }}
|
||||
</el-tag>
|
||||
<span v-if="scope.row.children.length > 2">
|
||||
+{{ scope.row.children.length - 2 }}个
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. 分组显示功能增强
|
||||
|
||||
#### 2.1 展开/收起功能
|
||||
- 主订单行支持展开查看商品明细
|
||||
- 展开内容显示完整的商品信息表格
|
||||
- 支持在展开内容中直接操作商品
|
||||
|
||||
#### 2.2 分组数据结构优化
|
||||
```javascript
|
||||
// 支持合并行的扁平化数据结构
|
||||
const flatList = [];
|
||||
Object.keys(grouped).forEach(mainOrderId => {
|
||||
const items = grouped[mainOrderId];
|
||||
// 创建主行(显示汇总信息)
|
||||
const mainRow = {
|
||||
...firstItem,
|
||||
isMainRow: true,
|
||||
rowspan: items.length,
|
||||
children: items
|
||||
};
|
||||
flatList.push(mainRow);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 订单详情功能完善
|
||||
|
||||
#### 3.1 详情对话框重新设计
|
||||
- **主订单信息卡片**:显示订单基本信息和汇总数据
|
||||
- **商品明细卡片**:支持商品数量修改、删除等操作
|
||||
- **订单统计卡片**:显示商品种类、总数量、总金额等统计信息
|
||||
|
||||
#### 3.2 商品管理功能
|
||||
- 支持修改商品数量(自动重新计算小计和汇总)
|
||||
- 支持删除订单中的商品
|
||||
- 支持添加新商品到订单(预留接口)
|
||||
|
||||
### 4. 搜索功能增强
|
||||
|
||||
#### 4.1 基础搜索优化
|
||||
- 添加订单状态筛选
|
||||
- 优化搜索条件布局
|
||||
- 支持回车键快速搜索
|
||||
|
||||
#### 4.2 高级搜索功能
|
||||
- 新增收货人、联系电话、商品类型等搜索条件
|
||||
- 支持售后状态筛选
|
||||
- 可展开/收起高级搜索表单
|
||||
|
||||
```html
|
||||
<!-- 高级搜索表单 -->
|
||||
<el-form v-show="showAdvancedSearch" class="advanced-search-form">
|
||||
<el-form-item label="收货人" prop="name">
|
||||
<el-input v-model="queryParams.name" placeholder="请输入收货人姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="queryParams.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<!-- 更多搜索条件... -->
|
||||
</el-form>
|
||||
```
|
||||
|
||||
### 5. 操作功能完善
|
||||
|
||||
#### 5.1 快速操作工具栏
|
||||
- **刷新数据**:重新加载订单信息
|
||||
- **导出数据**:导出当前筛选条件下的订单数据
|
||||
- **Excel批量发货**:原有的批量发货功能
|
||||
|
||||
#### 5.2 商品操作功能
|
||||
- 支持修改商品数量
|
||||
- 支持删除商品
|
||||
- 支持商品状态管理
|
||||
|
||||
## 🎨 界面设计改进
|
||||
|
||||
### 1. 视觉层次优化
|
||||
- 主订单行使用特殊背景色标识
|
||||
- 商品标签使用不同颜色区分状态
|
||||
- 卡片式布局提升信息层次感
|
||||
|
||||
### 2. 交互体验提升
|
||||
- 展开/收起动画效果
|
||||
- 悬停效果和阴影
|
||||
- 响应式布局适配
|
||||
|
||||
### 3. 样式系统完善
|
||||
```css
|
||||
/* 主订单行样式 */
|
||||
::v-deep .main-order-row {
|
||||
background-color: #f0f9ff !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 商品相关样式 */
|
||||
.product-summary {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-preview {
|
||||
margin-top: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 技术实现要点
|
||||
|
||||
### 1. 数据结构设计
|
||||
- 使用扁平化数据结构支持表格合并
|
||||
- 主行和子行分离,便于操作管理
|
||||
- 支持动态计算汇总数据
|
||||
|
||||
### 2. 组件交互
|
||||
- 使用 `span-method` 实现单元格合并
|
||||
- 使用 `expand-row-keys` 控制展开状态
|
||||
- 支持动态数据更新和重新计算
|
||||
|
||||
### 3. 性能优化
|
||||
- 按需加载商品详情
|
||||
- 支持数据缓存和刷新
|
||||
- 优化大量数据的渲染性能
|
||||
|
||||
## 📱 使用说明
|
||||
|
||||
### 1. 查看订单
|
||||
- **主订单行**:显示汇总信息,支持展开查看详情
|
||||
- **商品预览**:直接显示商品标签和数量
|
||||
- **详情查看**:点击"查看N个商品"进入详情页面
|
||||
|
||||
### 2. 管理商品
|
||||
- **数量修改**:在详情页面直接修改商品数量
|
||||
- **商品删除**:支持删除订单中的商品
|
||||
- **状态管理**:查看和管理商品状态
|
||||
|
||||
### 3. 搜索筛选
|
||||
- **基础搜索**:支持订单号、用户、商品等条件
|
||||
- **高级搜索**:展开后显示更多搜索选项
|
||||
- **快速筛选**:支持订单状态、售后状态等
|
||||
|
||||
### 4. 批量操作
|
||||
- **选择订单**:支持选择主订单(自动选择所有子订单)
|
||||
- **批量删除**:删除整个主订单或单个商品
|
||||
- **批量发货**:使用Excel批量处理发货
|
||||
|
||||
## 🎯 业务价值
|
||||
|
||||
### 1. 提升工作效率
|
||||
- 分组显示让订单管理更清晰
|
||||
- 快速查看和操作减少页面跳转
|
||||
- 批量操作支持提高处理效率
|
||||
|
||||
### 2. 改善用户体验
|
||||
- 直观的商品信息展示
|
||||
- 友好的交互操作界面
|
||||
- 完善的搜索和筛选功能
|
||||
|
||||
### 3. 支持业务需求
|
||||
- 按主订单分组管理符合业务逻辑
|
||||
- 支持商品级别的精细化管理
|
||||
- 提供完整的订单生命周期管理
|
||||
|
||||
## 🔮 后续优化建议
|
||||
|
||||
### 1. 功能扩展
|
||||
- 支持商品图片显示
|
||||
- 添加订单备注和标签功能
|
||||
- 支持订单模板和快速创建
|
||||
|
||||
### 2. 性能优化
|
||||
- 实现虚拟滚动支持大量数据
|
||||
- 添加数据缓存和预加载
|
||||
- 优化搜索和筛选性能
|
||||
|
||||
### 3. 用户体验
|
||||
- 添加操作引导和帮助提示
|
||||
- 支持个性化设置和偏好
|
||||
- 添加数据统计和图表展示
|
||||
|
||||
## 📋 修改文件清单
|
||||
|
||||
1. **前端页面**:`ruoyi-ui/src/views/system/GoodsOrder/index.vue`
|
||||
- 表格结构和显示逻辑
|
||||
- 商品信息展示优化
|
||||
- 分组显示功能实现
|
||||
- 详情对话框重新设计
|
||||
- 搜索功能增强
|
||||
|
||||
2. **样式文件**:同页面内嵌样式
|
||||
- 新增商品相关样式
|
||||
- 订单详情样式优化
|
||||
- 高级搜索表单样式
|
||||
|
||||
3. **说明文档**:`GoodsOrder功能改进说明.md`
|
||||
- 详细的功能说明
|
||||
- 使用方法和注意事项
|
||||
- 技术实现要点
|
||||
|
||||
## ✨ 总结
|
||||
|
||||
通过这次全面的功能改进,商品订单页面现在具备了:
|
||||
|
||||
- **清晰的分组显示**:按主订单号分组,支持展开查看详情
|
||||
- **完善的商品管理**:支持商品信息的查看、修改、删除等操作
|
||||
- **强大的搜索功能**:基础搜索+高级搜索,满足各种查询需求
|
||||
- **友好的用户界面**:现代化的卡片布局,直观的信息展示
|
||||
- **高效的操作体验**:支持批量操作,减少重复工作
|
||||
|
||||
这些改进完全满足了您提出的"按main_order_id分组查询显示"的需求,同时大大提升了页面的功能性和用户体验。
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
# UserSecondaryCard 次卡管理功能完善说明
|
||||
|
||||
## 已完成的功能
|
||||
|
||||
### 后端 Controller (UserSecondaryCardController.java)
|
||||
|
||||
#### 1. 基础CRUD功能 ✅
|
||||
- 查询次卡列表 (GET /list)
|
||||
- 获取次卡详情 (GET /{id})
|
||||
- 新增次卡 (POST /)
|
||||
- 修改次卡 (PUT /)
|
||||
- 删除次卡 (DELETE /{ids})
|
||||
- 导出次卡数据 (POST /export)
|
||||
|
||||
#### 2. 业务逻辑功能 ✅
|
||||
- 数字转中文工具方法 (`numberToChinese`)
|
||||
- 自动生成简介字段 (`generateIntroduction`)
|
||||
- 购买明细查询 (GET /purchaseDetails/{id})
|
||||
|
||||
#### 3. 新增功能 ✅
|
||||
- 批量状态更新 (PUT /batchStatus)
|
||||
- 批量删除 (DELETE /batchDelete)
|
||||
- 数据验证增强
|
||||
|
||||
#### 4. 数据验证 ✅
|
||||
- 总服务数必须大于0
|
||||
- 可提供服务数必须大于0
|
||||
- 可提供服务数不能大于总服务数
|
||||
- 实付价格不能大于展示价格
|
||||
|
||||
### 前端 Vue (index.vue)
|
||||
|
||||
#### 1. 界面功能 ✅
|
||||
- 次卡列表展示
|
||||
- 新增/修改对话框
|
||||
- 购买明细查看对话框
|
||||
- 图片上传和预览
|
||||
- 分页功能
|
||||
|
||||
#### 2. 搜索功能 ✅
|
||||
- 标题搜索
|
||||
- 分类筛选
|
||||
- 状态筛选
|
||||
- 价格范围搜索
|
||||
- 创建时间范围搜索
|
||||
|
||||
#### 3. 操作功能 ✅
|
||||
- 新增次卡
|
||||
- 修改次卡
|
||||
- 删除次卡
|
||||
- 导出数据
|
||||
- 批量状态更新
|
||||
- 快速状态切换(下拉菜单)
|
||||
|
||||
#### 4. 用户体验优化 ✅
|
||||
- 表单验证增强
|
||||
- 业务逻辑验证
|
||||
- 确认对话框
|
||||
- 错误提示
|
||||
- 加载状态
|
||||
- 图片错误处理
|
||||
|
||||
#### 5. 数据展示优化 ✅
|
||||
- 价格格式化显示
|
||||
- 图片缩略图展示
|
||||
- 状态标签显示
|
||||
- 服务数量统计
|
||||
- 时间格式化
|
||||
|
||||
## 技术特点
|
||||
|
||||
### 后端
|
||||
- 使用Spring Security进行权限控制
|
||||
- 集成MyBatis进行数据访问
|
||||
- 支持Excel导出
|
||||
- 完善的日志记录
|
||||
- 数据验证和错误处理
|
||||
|
||||
### 前端
|
||||
- 基于Vue.js + Element UI
|
||||
- 响应式设计
|
||||
- 组件化开发
|
||||
- 表单验证
|
||||
- 图片上传组件
|
||||
- 字典数据支持
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 1. 次卡管理
|
||||
- 可以创建、编辑、删除次卡
|
||||
- 支持图片上传(主图和轮播图)
|
||||
- 自动生成简介字段
|
||||
- 支持批量操作
|
||||
|
||||
### 2. 搜索筛选
|
||||
- 支持多条件组合搜索
|
||||
- 价格范围搜索
|
||||
- 时间范围搜索
|
||||
- 分类和状态筛选
|
||||
|
||||
### 3. 状态管理
|
||||
- 支持单个次卡状态快速切换
|
||||
- 支持批量状态更新
|
||||
- 状态变更有确认提示
|
||||
|
||||
### 4. 数据导出
|
||||
- 支持Excel格式导出
|
||||
- 导出数据包含所有字段
|
||||
- 支持搜索条件筛选导出
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限控制**: 所有操作都需要相应的权限
|
||||
2. **数据验证**: 前端和后端都有数据验证
|
||||
3. **图片处理**: 支持单张主图和多张轮播图
|
||||
4. **业务逻辑**: 自动计算服务数量和生成简介
|
||||
5. **错误处理**: 完善的错误提示和异常处理
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. 可以添加次卡使用统计功能
|
||||
2. 可以添加次卡到期提醒功能
|
||||
3. 可以添加次卡模板功能
|
||||
4. 可以添加批量导入功能
|
||||
5. 可以添加数据备份和恢复功能
|
||||
|
|
@ -7,8 +7,10 @@ import java.util.Map;
|
|||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ruoyi.system.ControllerUtil.OrderUtil;
|
||||
import com.ruoyi.system.domain.GoodsOrder;
|
||||
import com.ruoyi.system.domain.Order;
|
||||
import com.ruoyi.system.domain.OrderLog;
|
||||
import com.ruoyi.system.service.IGoodsOrderService;
|
||||
import com.ruoyi.system.service.IOrderLogService;
|
||||
import com.ruoyi.system.service.IOrderService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
|
@ -46,6 +48,8 @@ public class UsersPayBeforController extends BaseController
|
|||
private IOrderService orderService;
|
||||
@Autowired
|
||||
private IOrderLogService orderLogService;
|
||||
@Autowired
|
||||
private IGoodsOrderService goodsOrderService;
|
||||
|
||||
/**
|
||||
* 查询预支付列表
|
||||
|
|
@ -115,14 +119,38 @@ public class UsersPayBeforController extends BaseController
|
|||
return toAjax(usersPayBeforService.deleteUsersPayBeforByIds(ids));
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 根据订单ID查询预支付数据
|
||||
// */
|
||||
// @GetMapping("/getByOrderId/{orderId}")
|
||||
// public AjaxResult getByOrderId(@PathVariable("orderId") String orderId)
|
||||
// {
|
||||
// UsersPayBefor usersPayBefor = usersPayBeforService.selectUsersPayBeforByOrderId(orderId);
|
||||
// return success(usersPayBefor);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 根据订单ID查询预支付数据
|
||||
*/
|
||||
@GetMapping("/getByOrderId/{orderId}")
|
||||
public AjaxResult getByOrderId(@PathVariable("orderId") String orderId)
|
||||
{
|
||||
UsersPayBefor usersPayBefor = usersPayBeforService.selectUsersPayBeforByOrderId(orderId);
|
||||
return success(usersPayBefor);
|
||||
|
||||
GoodsOrder goodsOrder = new GoodsOrder();
|
||||
goodsOrder.setMainOrderId(orderId);
|
||||
List<GoodsOrder> orders = goodsOrderService.selectGoodsOrderList(goodsOrder);
|
||||
if (orders.size()>0){
|
||||
UsersPayBefor usersPayBefor = usersPayBeforService.selectUsersPayBeforByOrderId(orderId);
|
||||
if (usersPayBefor == null){
|
||||
usersPayBefor = usersPayBeforService.selectUsersPayBeforByOrderId(orders.getFirst().getOrderId());
|
||||
}
|
||||
return success(usersPayBefor);
|
||||
}else{
|
||||
return success();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1960,10 +1960,52 @@ public class OrderUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找用户首次下单
|
||||
* 根据用户ID查找该用户首次下单的订单ID
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 首次下单的订单ID,如果没有找到返回null
|
||||
*/
|
||||
public static String findUserFirstOrderId(Long userId) {
|
||||
try {
|
||||
if (userId == null) {
|
||||
System.err.println("用户ID不能为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
IOrderService orderService = SpringUtils.getBean(IOrderService.class);
|
||||
String firstOrderId = orderService.selectUserFirstOrderId(userId);
|
||||
|
||||
if (firstOrderId != null) {
|
||||
System.out.println("用户 " + userId + " 首次下单订单ID: " + firstOrderId);
|
||||
} else {
|
||||
System.out.println("用户 " + userId + " 没有找到首次下单记录");
|
||||
}
|
||||
|
||||
return firstOrderId;
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("查找用户首次下单异常,用户ID: " + userId + ", 错误: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找用户首次下单(重载方法,支持用户对象)
|
||||
* 根据用户对象查找该用户首次下单的订单ID
|
||||
*
|
||||
* @param user 用户对象
|
||||
* @return 首次下单的订单ID,如果没有找到返回null
|
||||
*/
|
||||
public static String findUserFirstOrderId(Users user) {
|
||||
if (user == null || user.getId() == null) {
|
||||
System.err.println("用户对象或用户ID为空");
|
||||
return null;
|
||||
}
|
||||
return findUserFirstOrderId(user.getId());
|
||||
}
|
||||
|
||||
// public static void main(String[] args) {
|
||||
// // 构造一个测试用的json字符串
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ public class WechatPayUtil {
|
|||
private static final String WECHAT_TRANSFER_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"; // 企业付款
|
||||
|
||||
|
||||
public static final String PAY_FH = "https://api.huafurenjia.cn/";
|
||||
public static final String PAY_FH = "https://403e667e.r3.cpolar.top";
|
||||
/**
|
||||
* 其他配置常量
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
securityJsCode: "8c58e51cb91b527f0fb863b3c97ef3c7",
|
||||
};
|
||||
</script>
|
||||
<script src="https://webapi.amap.com/maps?v=2.0&key=03e2077b2c18a2ddeaadf34c434f75d4&plugin=AMap.Geocoder"></script>
|
||||
<script src="https://webapi.amap.com/maps?v=2.0&key=03e2077b2c18a2ddeaadf34c434f75d4&plugin=AMap.Geocoder,AMap.PlaceSearch"></script>
|
||||
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= webpackConfig.name %></title>
|
||||
|
|
|
|||
|
|
@ -177,7 +177,9 @@ aside {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-drawer__body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
/* 修复对话框遮罩层问题 */
|
||||
//.v-modal {
|
||||
// z-index: 9998 !important;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import AddressSelector from './index.vue'
|
||||
|
||||
export default AddressSelector
|
||||
|
|
@ -0,0 +1,755 @@
|
|||
<template>
|
||||
<div class="address-selector">
|
||||
<div class="map-container">
|
||||
<div class="map-info">
|
||||
<span>地址: {{ address || "未设置" }}</span>
|
||||
<span>经度: {{ longitude || "未设置" }}</span>
|
||||
<span>纬度: {{ latitude || "未设置" }}</span>
|
||||
<span v-if="disabled" class="locked-status">
|
||||
<i class="el-icon-view"></i> 只读模式
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="showMapSelector"
|
||||
icon="el-icon-location"
|
||||
>
|
||||
{{ disabled ? '查看位置' : '选择位置' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 地图选择对话框 -->
|
||||
<el-dialog
|
||||
:title="disabled ? '查看地址' : '选择地址'"
|
||||
:visible.sync="showMapDialog"
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
@opened="onDialogOpened"
|
||||
>
|
||||
<div class="map-dialog-content">
|
||||
<div class="map-search">
|
||||
<el-input
|
||||
v-model="mapSearchKeyword"
|
||||
placeholder="搜索地址"
|
||||
style="width: 300px; margin-right: 8px"
|
||||
size="small"
|
||||
@keyup.enter.native="searchMapAddress"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<el-button
|
||||
slot="append"
|
||||
@click="searchMapAddress"
|
||||
icon="el-icon-search"
|
||||
:disabled="disabled"
|
||||
></el-button>
|
||||
</el-input>
|
||||
<el-button
|
||||
@click="useCurrentLocation"
|
||||
type="success"
|
||||
size="small"
|
||||
icon="el-icon-location"
|
||||
:disabled="disabled"
|
||||
>
|
||||
使用当前位置
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="map-container-main">
|
||||
<div :id="mapId" class="map-view">
|
||||
<div v-if="!mapInited" class="map-loading">
|
||||
<i class="el-icon-loading"></i>
|
||||
<p>地图加载中...</p>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="forceInitMap"
|
||||
style="margin-top: 10px;"
|
||||
>
|
||||
手动初始化地图
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-sidebar">
|
||||
<div class="search-results" v-if="searchResults.length > 0">
|
||||
<h4>搜索结果 ({{ searchResults.length }})</h4>
|
||||
<div
|
||||
v-for="(item, index) in searchResults"
|
||||
:key="index"
|
||||
class="search-item"
|
||||
@click="selectSearchResult(item)"
|
||||
:class="{ 'disabled-item': disabled }"
|
||||
>
|
||||
<div class="result-title">{{ item.title }}</div>
|
||||
<div class="result-address">{{ item.address }}</div>
|
||||
<div class="result-type">{{ item.type }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="mapSearchKeyword && !searchResults.length" class="no-results">
|
||||
<p>未找到相关地址</p>
|
||||
<p>请尝试更详细的关键词</p>
|
||||
</div>
|
||||
<div v-else class="search-tips">
|
||||
<h4>搜索提示</h4>
|
||||
<p>• 输入地址关键词进行搜索</p>
|
||||
<p>• 支持城市、街道、地标等</p>
|
||||
<p>• 点击搜索结果可定位到地图</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selected-location" v-if="selectedLocation">
|
||||
<h4>已选择位置</h4>
|
||||
<p><strong>地址:</strong> {{ selectedLocation.address }}</p>
|
||||
<p><strong>经度:</strong> {{ selectedLocation.lng }}</p>
|
||||
<p><strong>纬度:</strong> {{ selectedLocation.lat }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="showMapDialog = false">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="confirmLocation"
|
||||
:disabled="!selectedLocation || disabled"
|
||||
>
|
||||
{{ disabled ? '位置已锁定' : '确认选择' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AddressSelector",
|
||||
props: {
|
||||
// 经度
|
||||
longitude: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
// 纬度
|
||||
latitude: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
// 地址
|
||||
address: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
// 是否禁用
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMapDialog: false,
|
||||
mapSearchKeyword: "",
|
||||
searchResults: [],
|
||||
selectedLocation: null,
|
||||
map: null,
|
||||
marker: null,
|
||||
geocoder: null,
|
||||
placeSearch: null, // 添加地点搜索插件
|
||||
mapInited: false,
|
||||
mapId: `map-${Date.now()}`, // 添加唯一的ID
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// 监听外部传入的经纬度变化
|
||||
longitude(newVal) {
|
||||
if (newVal && this.map) {
|
||||
this.updateMapCenter();
|
||||
}
|
||||
},
|
||||
latitude(newVal) {
|
||||
if (newVal && this.map) {
|
||||
this.updateMapCenter();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
// 移除事件监听器
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
methods: {
|
||||
/** 显示地图对话框 */
|
||||
showMapSelector() {
|
||||
this.showMapDialog = true;
|
||||
// 地图初始化将在对话框打开后自动触发
|
||||
},
|
||||
|
||||
/** 初始化地图 */
|
||||
initMap() {
|
||||
console.log('开始初始化地图...', this.mapId);
|
||||
|
||||
// 销毁旧地图和事件,防止重复绑定
|
||||
if (this.map) {
|
||||
console.log('销毁旧地图...');
|
||||
this.map.destroy();
|
||||
this.map = null;
|
||||
this.marker = null;
|
||||
this.geocoder = null;
|
||||
this.placeSearch = null;
|
||||
this.mapInited = false;
|
||||
}
|
||||
|
||||
// 检查高德地图API是否可用
|
||||
if (!window.AMap) {
|
||||
console.error('高德地图API未加载');
|
||||
this.$modal.msgError("地图加载失败,请检查网络连接");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('高德地图API已加载,开始创建地图实例...');
|
||||
|
||||
try {
|
||||
// 确保地图容器存在
|
||||
const mapContainer = document.getElementById(this.mapId);
|
||||
if (!mapContainer) {
|
||||
console.error('地图容器不存在:', this.mapId);
|
||||
this.$modal.msgError("地图容器不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('地图容器找到:', mapContainer);
|
||||
|
||||
// 设置地图容器的样式
|
||||
mapContainer.style.width = '100%';
|
||||
mapContainer.style.height = '100%';
|
||||
mapContainer.style.minHeight = '400px';
|
||||
mapContainer.style.position = 'relative';
|
||||
|
||||
// 等待一下确保DOM完全渲染
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 创建地图实例
|
||||
this.map = new window.AMap.Map(this.mapId, {
|
||||
zoom: 16,
|
||||
center: [this.longitude || 108.94141, this.latitude || 34.209883],
|
||||
viewMode: '2D',
|
||||
resizeEnable: true,
|
||||
dragEnable: true,
|
||||
zoomEnable: true,
|
||||
doubleClickZoom: true,
|
||||
keyboardEnable: false,
|
||||
jogEnable: true,
|
||||
scrollWheel: true,
|
||||
touchZoom: true,
|
||||
showIndoorMap: false,
|
||||
});
|
||||
|
||||
console.log('地图实例创建成功');
|
||||
|
||||
// 创建地理编码器
|
||||
this.geocoder = new window.AMap.Geocoder();
|
||||
|
||||
// 创建地点搜索插件
|
||||
this.placeSearch = new window.AMap.PlaceSearch({
|
||||
city: '全国',
|
||||
pageSize: 10,
|
||||
pageIndex: 1
|
||||
});
|
||||
|
||||
// 如果有已保存的坐标,设置地图中心
|
||||
if (this.longitude && this.latitude) {
|
||||
console.log('设置已保存的坐标:', this.longitude, this.latitude);
|
||||
this.setMapMarker([this.longitude, this.latitude]);
|
||||
this.map.setCenter([this.longitude, this.latitude]);
|
||||
}
|
||||
|
||||
// 添加点击事件
|
||||
this.map.on("click", (e) => {
|
||||
if (this.disabled) {
|
||||
this.$message.warning('位置已锁定,无法修改');
|
||||
return;
|
||||
}
|
||||
console.log('地图点击事件:', e.lnglat);
|
||||
const lnglat = [e.lnglat.lng, e.lnglat.lat];
|
||||
this.setMapMarker(lnglat);
|
||||
this.getAddressFromCoords(e.lnglat.lng, e.lnglat.lat);
|
||||
});
|
||||
|
||||
// 添加地图加载完成事件
|
||||
this.map.on('complete', () => {
|
||||
console.log('地图加载完成');
|
||||
this.mapInited = true;
|
||||
});
|
||||
|
||||
// 添加地图错误事件
|
||||
this.map.on('error', (error) => {
|
||||
console.error('地图加载错误:', error);
|
||||
});
|
||||
|
||||
// 强制触发地图重绘
|
||||
setTimeout(() => {
|
||||
if (this.map) {
|
||||
this.map.resize();
|
||||
console.log('地图重绘完成');
|
||||
|
||||
// 再次确保地图完全填充容器
|
||||
setTimeout(() => {
|
||||
if (this.map) {
|
||||
this.map.resize();
|
||||
console.log('地图二次重绘完成');
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('地图实例创建失败:', error);
|
||||
this.$modal.msgError("地图初始化失败: " + error.message);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
} catch (error) {
|
||||
console.error('地图初始化失败:', error);
|
||||
this.$modal.msgError("地图初始化失败: " + error.message);
|
||||
}
|
||||
},
|
||||
|
||||
/** 更新地图中心点 */
|
||||
updateMapCenter() {
|
||||
if (this.map && this.longitude && this.latitude) {
|
||||
this.map.setCenter([this.longitude, this.latitude]);
|
||||
this.setMapMarker([this.longitude, this.latitude]);
|
||||
}
|
||||
},
|
||||
|
||||
/** 设置地图标记 */
|
||||
setMapMarker(lnglat) {
|
||||
if (this.marker) {
|
||||
this.marker.setMap(null);
|
||||
}
|
||||
this.marker = new window.AMap.Marker({
|
||||
position: lnglat,
|
||||
map: this.map,
|
||||
});
|
||||
},
|
||||
|
||||
/** 根据坐标获取地址信息 */
|
||||
getAddressFromCoords(lng, lat) {
|
||||
if (!this.geocoder) return;
|
||||
|
||||
this.geocoder.getAddress([lng, lat], (status, result) => {
|
||||
if (status === "complete" && result.regeocode) {
|
||||
const address = result.regeocode.formattedAddress;
|
||||
this.selectedLocation = {
|
||||
lng: lng,
|
||||
lat: lat,
|
||||
address: address,
|
||||
};
|
||||
} else {
|
||||
this.selectedLocation = {
|
||||
lng: lng,
|
||||
lat: lat,
|
||||
address: "未知地址",
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** 搜索地址 */
|
||||
searchMapAddress() {
|
||||
if (this.disabled) {
|
||||
this.$message.warning('位置已锁定,无法修改');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.mapSearchKeyword.trim()) {
|
||||
this.$message.warning("请输入搜索关键词");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.mapInited || !this.placeSearch) {
|
||||
this.$message.warning("地图未初始化,请稍后再试");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始搜索地址:', this.mapSearchKeyword);
|
||||
|
||||
// 使用PlaceSearch插件搜索地址
|
||||
this.placeSearch.search(this.mapSearchKeyword, (status, result) => {
|
||||
console.log('搜索状态:', status, '搜索结果:', result);
|
||||
|
||||
if (status === "complete" && result.info === "OK" && result.poiList && result.poiList.pois) {
|
||||
// 处理搜索结果
|
||||
this.searchResults = result.poiList.pois.map((poi) => ({
|
||||
title: poi.name,
|
||||
address: poi.address || poi.pname + poi.cityname + poi.adname,
|
||||
lng: poi.location.lng,
|
||||
lat: poi.location.lat,
|
||||
type: poi.type
|
||||
}));
|
||||
|
||||
console.log('搜索结果数量:', this.searchResults.length);
|
||||
|
||||
if (this.searchResults.length > 0) {
|
||||
// 自动选择第一个结果并定位
|
||||
const firstResult = this.searchResults[0];
|
||||
this.map.setCenter([firstResult.lng, firstResult.lat]);
|
||||
this.setMapMarker([firstResult.lng, firstResult.lat]);
|
||||
this.getAddressFromCoords(firstResult.lng, firstResult.lat);
|
||||
}
|
||||
} else {
|
||||
this.searchResults = [];
|
||||
this.$message.warning("未找到该地址,请输入更详细的地址");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** 选择搜索结果 */
|
||||
selectSearchResult(item) {
|
||||
if (this.disabled) {
|
||||
this.$message.warning('位置已锁定,无法修改');
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedLocation = {
|
||||
lng: item.lng,
|
||||
lat: item.lat,
|
||||
address: item.address,
|
||||
};
|
||||
|
||||
// 移动地图到选中位置
|
||||
this.map.setCenter([item.lng, item.lat]);
|
||||
this.setMapMarker([item.lng, item.lat]);
|
||||
},
|
||||
|
||||
/** 使用当前位置 */
|
||||
useCurrentLocation() {
|
||||
if (this.disabled) {
|
||||
this.$message.warning('位置已锁定,无法修改');
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const lng = position.coords.longitude;
|
||||
const lat = position.coords.latitude;
|
||||
|
||||
this.map.setCenter([lng, lat]);
|
||||
this.setMapMarker([lng, lat]);
|
||||
this.getAddressFromCoords(lng, lat);
|
||||
},
|
||||
(error) => {
|
||||
this.$modal.msgError("获取当前位置失败: " + error.message);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.$modal.msgError("浏览器不支持地理位置功能");
|
||||
}
|
||||
},
|
||||
|
||||
/** 确认选择位置 */
|
||||
confirmLocation() {
|
||||
if (this.selectedLocation) {
|
||||
// 向父组件发送选择结果
|
||||
this.$emit("location-selected", {
|
||||
longitude: this.selectedLocation.lng,
|
||||
latitude: this.selectedLocation.lat,
|
||||
address: this.selectedLocation.address,
|
||||
});
|
||||
|
||||
this.showMapDialog = false;
|
||||
this.$modal.msgSuccess("位置选择成功");
|
||||
}
|
||||
},
|
||||
|
||||
/** 对话框打开时初始化地图 */
|
||||
onDialogOpened() {
|
||||
console.log('地图对话框已打开,尝试初始化地图...');
|
||||
this.initMap();
|
||||
},
|
||||
|
||||
/** 强制重新初始化地图 */
|
||||
forceInitMap() {
|
||||
console.log('强制重新初始化地图...');
|
||||
this.mapInited = false; // 强制地图销毁
|
||||
this.initMap(); // 重新初始化
|
||||
},
|
||||
|
||||
/** 处理窗口大小变化 */
|
||||
handleResize() {
|
||||
if (this.map) {
|
||||
this.map.resize();
|
||||
console.log('地图容器大小变化,触发地图重绘');
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.address-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 地图容器样式 */
|
||||
.map-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.map-info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.map-info span {
|
||||
background: #f5f7fa;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.locked-status {
|
||||
color: #909399 !important;
|
||||
font-weight: 500;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: #f4f4f5 !important;
|
||||
border-color: #d3d4d6 !important;
|
||||
}
|
||||
|
||||
.locked-status i {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 地图对话框样式 */
|
||||
.map-dialog-content {
|
||||
height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.map-search {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.map-container-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
height: 400px;
|
||||
min-height: 400px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.map-view {
|
||||
flex: 1;
|
||||
height: 400px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 400px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 动态地图容器样式 */
|
||||
[id^="map-"] {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
min-height: 400px;
|
||||
position: relative !important;
|
||||
border-radius: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.map-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.map-loading i {
|
||||
font-size: 40px;
|
||||
color: #409eff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.map-loading p {
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.map-sidebar {
|
||||
width: 300px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-results h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-item {
|
||||
padding: 10px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.search-item:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-item.disabled-item {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.search-item.disabled-item:hover {
|
||||
border-color: #e4e7ed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.result-address {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.result-type {
|
||||
font-size: 10px;
|
||||
color: #606266;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-tips {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background: #f0f9eb;
|
||||
border: 1px solid #e1f3d8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-tips h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #67c23a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-tips p {
|
||||
margin: 5px 0;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.selected-location {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #b3d8ff;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.selected-location h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.selected-location p {
|
||||
margin: 5px 0;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.map-container-main {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.map-sidebar {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.map-view {
|
||||
height: 300px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
[id^="map-"] {
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.map-dialog-content {
|
||||
height: 400px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.map-view {
|
||||
height: 250px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
[id^="map-"] {
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
<template>
|
||||
<el-dialog :title="title" :visible.sync="visible" width="80%" append-to-body @close="handleClose">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
|
||||
<!-- 主订单信息 -->
|
||||
<el-card shadow="hover" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #409EFF;">
|
||||
<i class="el-icon-s-order"></i>
|
||||
主订单信息
|
||||
</span>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="主订单号" prop="mainOrderId">
|
||||
<el-input v-model="form.mainOrderId" placeholder="请输入主订单号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户" prop="uid">
|
||||
<user-select
|
||||
v-model="form.uid"
|
||||
placeholder="请选择用户"
|
||||
user-type="1"
|
||||
dialog-title="选择用户"
|
||||
@change="handleUserSelectChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收货人" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入收货人姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="收货地址" prop="address">
|
||||
<el-input v-model="form.address" type="textarea" placeholder="请输入收货地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="mark">
|
||||
<el-input v-model="form.mark" type="textarea" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 子订单列表 -->
|
||||
<el-card shadow="hover" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #67C23A;">
|
||||
<i class="el-icon-goods"></i>
|
||||
商品信息列表
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="subOrdersList" border style="width: 100%">
|
||||
<el-table-column label="序号" type="index" width="60" align="center" />
|
||||
<el-table-column label="商品" prop="productId" min-width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-select
|
||||
v-model="scope.row.productId"
|
||||
placeholder="请选择商品"
|
||||
clearable
|
||||
:disabled="true"
|
||||
filterable
|
||||
size="mini"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in goodsDataList"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" prop="num" width="120" align="center"> </el-table-column>
|
||||
<el-table-column label="金额" prop="num" width="120" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span class="num">{{ scope.row.totalPrice ? scope.row.totalPrice.toFixed(2) : '0.00' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 订单汇总信息 -->
|
||||
<el-card shadow="hover" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #E6A23C;">
|
||||
<i class="el-icon-data-analysis"></i>
|
||||
订单汇总
|
||||
</span>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="summary-item">
|
||||
<label>总金额:</label>
|
||||
<span class="value price">¥{{ totalSubOrdersPrice.toFixed(2) }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="summary-item">
|
||||
<label>支付金额:</label>
|
||||
<span class="value price">¥{{ form.payPrice ? form.payPrice.toFixed(2) : '0.00' }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<div class="dialog-footer" style="text-align:left;margin-top:20px;">
|
||||
<el-button @click="reset">重置</el-button>
|
||||
<el-button type="primary" @click="submitForm">提交</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AddEditDialog",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
subOrdersList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
goodsDataList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalSubOrdersPrice() {
|
||||
return this.subOrdersList.reduce((total, order) => {
|
||||
return total + (order.totalPrice || 0);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
handleUserSelectChange(user) {
|
||||
this.$emit('user-select-change', user);
|
||||
},
|
||||
reset() {
|
||||
this.$emit('reset');
|
||||
},
|
||||
submitForm() {
|
||||
this.$emit('submit');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.summary-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.summary-item label {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.summary-item .value {
|
||||
font-size: 16px;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.summary-item .value.price {
|
||||
color: #E6A23C;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.num {
|
||||
color: #409EFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="售后详情"
|
||||
:visible.sync="localVisible"
|
||||
width="60%"
|
||||
append-to-body
|
||||
>
|
||||
<div v-if="currentAfterSaleOrder" class="after-sale-detail">
|
||||
<el-card class="box-card" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #409EFF;">📋 订单基本信息</span>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<p><strong>订单号:</strong>{{ currentAfterSaleOrder.orderId }}</p>
|
||||
<p><strong>商品名称:</strong>{{ currentAfterSaleOrder.productName }}</p>
|
||||
<p><strong>订单金额:</strong>¥{{ currentAfterSaleOrder.payPrice ? currentAfterSaleOrder.payPrice.toFixed(2) : '0.00' }}</p>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<p><strong>用户姓名:</strong>{{ currentAfterSaleOrder.name }}</p>
|
||||
<p><strong>联系电话:</strong>{{ currentAfterSaleOrder.phone }}</p>
|
||||
<p><strong>订单状态:</strong>
|
||||
<el-tag :type="currentAfterSaleOrder.status >= 20 ? 'warning' : 'success'">
|
||||
{{ currentAfterSaleOrder.status >= 20 ? '售后中' : '正常' }}
|
||||
</el-tag>
|
||||
</p>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #E6A23C;">🛠️ 售后信息</span>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<p><strong>售后类别:</strong>
|
||||
<el-tag v-if="currentAfterSaleOrder.returntype === 1" type="info">仅退款</el-tag>
|
||||
<el-tag v-else-if="currentAfterSaleOrder.returntype === 2" type="warning">退货退款</el-tag>
|
||||
<span v-else>未设置</span>
|
||||
</p>
|
||||
<p><strong>退款金额:</strong>¥{{ currentAfterSaleOrder.returnmoney || '0.00' }}</p>
|
||||
<p><strong>申请时间:</strong>{{ currentAfterSaleOrder.returntime ? parseTime(currentAfterSaleOrder.returntime, '{y}-{m}-{d} {h}:{i}:{s}') : '未设置' }}</p>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<p><strong>售后状态:</strong>
|
||||
<el-tag
|
||||
:type="getReturnStatusType(currentAfterSaleOrder.returnstatus)"
|
||||
v-if="currentAfterSaleOrder.returnstatus"
|
||||
>
|
||||
{{ returnStatusMap[currentAfterSaleOrder.returnstatus] || '未知状态' }}
|
||||
</el-tag>
|
||||
<span v-else>未申请售后</span>
|
||||
</p>
|
||||
<p><strong>退货快递:</strong>{{ currentAfterSaleOrder.returnlogistics || '未设置' }}</p>
|
||||
<p><strong>退货快递号:</strong>{{ currentAfterSaleOrder.returnlogisticscode || '未设置' }}</p>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card" style="margin-bottom: 20px;" v-if="currentAfterSaleOrder.returnreason">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #F56C6C;">📝 售后原因</span>
|
||||
</div>
|
||||
<div style="padding: 10px; background-color: #f8f9fa; border-radius: 4px;">
|
||||
{{ currentAfterSaleOrder.returnreason }}
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<!-- 售后申请操作按钮区域 -->
|
||||
<div v-if="showAfterSaleActions" class="action-buttons">
|
||||
<el-button type="danger" @click="handleRejectAfterSale">
|
||||
<i class="el-icon-close"></i> 驳回申请
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleApproveAfterSale">
|
||||
<i class="el-icon-check"></i> 同意申请
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 平台收货后操作按钮区域 -->
|
||||
<div v-if="showPlatformActions" class="action-buttons">
|
||||
<el-button type="danger" @click="handleRejectRefund">
|
||||
<i class="el-icon-close"></i> 驳回退款
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleApproveRefund">
|
||||
<i class="el-icon-check"></i> 同意退款
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AfterSaleDialog",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentAfterSaleOrder: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
showAfterSaleActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showPlatformActions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
returnStatusMap: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
getReturnStatusType(status) {
|
||||
// 根据状态返回对应的标签类型
|
||||
const statusTypeMap = {
|
||||
1: 'info', // 待审核
|
||||
2: 'warning', // 审核通过
|
||||
3: 'danger', // 审核拒绝
|
||||
4: 'success' // 已完成
|
||||
};
|
||||
return statusTypeMap[status] || 'info';
|
||||
},
|
||||
handleRejectAfterSale() {
|
||||
this.$emit('reject-after-sale');
|
||||
},
|
||||
handleApproveAfterSale() {
|
||||
this.$emit('approve-after-sale');
|
||||
},
|
||||
handleRejectRefund() {
|
||||
this.$emit('reject-refund');
|
||||
},
|
||||
handleApproveRefund() {
|
||||
this.$emit('approve-refund');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.after-sale-detail p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.after-sale-detail strong {
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="Excel批量发货"
|
||||
:visible.sync="localVisible"
|
||||
width="60%"
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="excelImportForm" :model="form" :rules="rules" label-width="120px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="Excel文件" prop="file">
|
||||
<el-upload
|
||||
ref="upload"
|
||||
class="upload-demo"
|
||||
:action="''"
|
||||
:auto-upload="false"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
:before-upload="beforeUpload"
|
||||
:file-list="form.file ? [form.file] : []"
|
||||
accept=".xlsx,.xls"
|
||||
:limit="1"
|
||||
>
|
||||
<el-button size="small" type="primary">选择文件</el-button>
|
||||
<div slot="tip" class="el-upload__tip">
|
||||
只能上传xlsx/xls文件,且不超过2MB
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="操作说明">
|
||||
<div style="background-color: #f5f7fa; padding: 15px; border-radius: 4px; border-left: 4px solid #409EFF;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #409EFF;">📋 Excel文件格式要求:</h4>
|
||||
<p style="margin: 5px 0; color: #606266;">
|
||||
<strong>第1列:</strong>订单号(必填)
|
||||
</p>
|
||||
<p style="margin: 5px 0; color: #606266;">
|
||||
<strong>第2列:</strong>快递公司名称(必填)
|
||||
</p>
|
||||
<p style="margin: 5px 0; color: #606266;">
|
||||
<strong>第3列:</strong>快递单号(必填)
|
||||
</p>
|
||||
<p style="margin: 5px 0; color: #606266;">
|
||||
<strong>第4列:</strong>发货时间(必填,格式:yyyy-MM-dd)
|
||||
</p>
|
||||
<p style="margin: 5px 0; color: #606266;">
|
||||
<strong>注意:</strong>只有状态为"已支付待发货"的订单才能进行批量发货操作
|
||||
</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="downloadTemplate" type="info" icon="el-icon-download">
|
||||
下载CSV模板
|
||||
</el-button>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:loading="loading"
|
||||
:disabled="!form.file"
|
||||
>
|
||||
开始导入
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ExcelImportDialog",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
file: null
|
||||
})
|
||||
},
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCancel() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$emit('confirm', this.form);
|
||||
},
|
||||
handleFileChange(file) {
|
||||
this.$emit('file-change', file);
|
||||
},
|
||||
handleFileRemove() {
|
||||
this.$emit('file-remove');
|
||||
},
|
||||
beforeUpload(file) {
|
||||
this.$emit('before-upload', file);
|
||||
return false; // 阻止自动上传
|
||||
},
|
||||
downloadTemplate() {
|
||||
this.$emit('download-template');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="导入结果"
|
||||
:visible.sync="localVisible"
|
||||
width="60%"
|
||||
append-to-body
|
||||
>
|
||||
<div v-if="importResult" class="import-result">
|
||||
<el-result
|
||||
:icon="importResult.success > 0 ? 'success' : 'warning'"
|
||||
:title="importResult.success > 0 ? '批量发货完成' : '批量发货失败'"
|
||||
:sub-title="`共处理 ${importResult.total} 个订单`"
|
||||
>
|
||||
<template slot="extra">
|
||||
<el-row :gutter="20" style="width: 100%;">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" style="text-align: center;">
|
||||
<div style="font-size: 24px; color: #67C23A; font-weight: bold;">
|
||||
{{ importResult.success }}
|
||||
</div>
|
||||
<div style="color: #606266;">成功发货</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" style="text-align: center;">
|
||||
<div style="font-size: 24px; color: #F56C6C; font-weight: bold;">
|
||||
{{ importResult.fail }}
|
||||
</div>
|
||||
<div style="color: #606266;">发货失败</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" style="text-align: center;">
|
||||
<div style="font-size: 24px; color: #E6A23C; font-weight: bold;">
|
||||
{{ importResult.skip }}
|
||||
</div>
|
||||
<div style="color: #606266;">跳过处理</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<el-collapse v-if="importResult.failList && importResult.failList.length > 0">
|
||||
<el-collapse-item title="失败详情" name="1">
|
||||
<el-tag
|
||||
v-for="(item, index) in importResult.failList"
|
||||
:key="index"
|
||||
type="danger"
|
||||
style="margin: 5px;"
|
||||
>
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<el-collapse v-if="importResult.skipList && importResult.skipList.length > 0">
|
||||
<el-collapse-item title="跳过详情" name="2">
|
||||
<el-tag
|
||||
v-for="(item, index) in importResult.skipList"
|
||||
:key="index"
|
||||
type="warning"
|
||||
style="margin: 5px;"
|
||||
>
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-empty description="暂无导入结果"></el-empty>
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" @click="handleRefresh">刷新订单列表</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ImportResultDialog",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
importResult: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
handleRefresh() {
|
||||
this.$emit('refresh');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-result {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<el-dialog title="预支付数据" :visible.sync="localVisible" width="80%" append-to-body>
|
||||
<div v-if="prePaymentData" class="pre-payment-data">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="订单ID">{{ prePaymentData.orderid }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付时间">{{ parseTime(prePaymentData.paytime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 金额信息 -->
|
||||
<el-divider content-position="left">金额信息</el-divider>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="总金额">
|
||||
<span style="color: #E6A23C; font-weight: bold;">¥{{ prePaymentData.allmoney ? prePaymentData.allmoney.toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="微信支付金额">
|
||||
<span style="color: #67C23A;">¥{{ prePaymentData.wxmoney ? prePaymentData.wxmoney.toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="余额支付金额">
|
||||
<span style="color: #409EFF;">¥{{ prePaymentData.yemoney ? prePaymentData.yemoney.toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会员优惠金额">
|
||||
<span style="color: #F56C6C;">¥{{ prePaymentData.membermoney ? prePaymentData.membermoney.toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="购物金抵扣金额">
|
||||
<span style="color: #909399;">¥{{ prePaymentData.shopmoney ? prePaymentData.shopmoney.toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务金抵扣金额">
|
||||
<span style="color: #E6A23C;">¥{{ prePaymentData.servicemoney ? prePaymentData.servicemoney.toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="优惠券金额">
|
||||
<span style="color: #F56C6C;">¥{{ prePaymentData.couponmoney ? prePaymentData.couponmoney.toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="美团抵扣金额">
|
||||
<span style="color: #67C23A;">¥{{ prePaymentData.mtmoney ? prePaymentData.mtmoney.toFixed(2) : '0.00' }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-empty description="暂无预支付数据"></el-empty>
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "PrePaymentDialog",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
prePaymentData: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
parseTime(time, format) {
|
||||
if (!time) return '';
|
||||
// 这里应该使用项目中的时间格式化函数
|
||||
// 暂时返回原始时间,实际使用时需要导入相应的工具函数
|
||||
return time;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pre-payment-data {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="退款设置"
|
||||
:visible.sync="localVisible"
|
||||
width="60%"
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="refundAmountForm" :model="form" :rules="rules" label-width="120px">
|
||||
<!-- 订单金额信息 -->
|
||||
<el-divider content-position="left">订单金额信息</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="订单总金额">
|
||||
<el-input
|
||||
v-model="form.orderAmount"
|
||||
disabled
|
||||
style="color: #409EFF; font-weight: bold;"
|
||||
>
|
||||
<template slot="prepend">¥</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="已支付金额">
|
||||
<el-input
|
||||
v-model="form.payAmount"
|
||||
disabled
|
||||
style="color: #67C23A; font-weight: bold;"
|
||||
>
|
||||
<template slot="prepend">¥</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 退款类型设置 -->
|
||||
<el-divider content-position="left">退款类型设置</el-divider>
|
||||
|
||||
<!-- 金额退款 -->
|
||||
<el-form-item label="金额退款" prop="moneyRefund">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="form.enableMoneyRefund">启用金额退款</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-input
|
||||
v-model="form.moneyRefund"
|
||||
type="number"
|
||||
placeholder="请输入退款金额"
|
||||
:min="0"
|
||||
:max="form.payAmount"
|
||||
step="0.01"
|
||||
:disabled="!form.enableMoneyRefund"
|
||||
>
|
||||
<template slot="prepend">¥</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 余额退款 -->
|
||||
<el-form-item label="余额退款" prop="balanceRefund">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="form.enableBalanceRefund">启用余额退款</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-input
|
||||
v-model="form.balanceRefund"
|
||||
type="number"
|
||||
placeholder="请输入余额退款金额"
|
||||
:min="0"
|
||||
:max="form.balanceAmount || 0"
|
||||
step="0.01"
|
||||
:disabled="!form.enableBalanceRefund"
|
||||
>
|
||||
<template slot="prepend">¥</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 优惠券返还 -->
|
||||
<el-form-item label="优惠券返还" prop="couponRefund">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="form.enableCouponRefund">启用优惠券返还</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-input
|
||||
v-model="form.couponRefund"
|
||||
type="number"
|
||||
placeholder="请输入优惠券返还金额"
|
||||
:min="0"
|
||||
:max="form.couponAmount || 0"
|
||||
step="0.01"
|
||||
:disabled="!form.enableCouponRefund"
|
||||
>
|
||||
<template slot="prepend">¥</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 购物金返还 -->
|
||||
<el-form-item label="购物金返还" prop="shoppingRefund">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="form.enableShoppingRefund">启用购物金返还</el-checkbox>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-input
|
||||
v-model="form.shoppingRefund"
|
||||
type="number"
|
||||
placeholder="请输入购物金返还金额"
|
||||
:min="0"
|
||||
:max="form.shoppingAmount || 0"
|
||||
step="0.01"
|
||||
:disabled="!form.enableShoppingRefund"
|
||||
>
|
||||
<template slot="prepend">¥</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 退款备注 -->
|
||||
<el-form-item label="退款备注" prop="refundRemark">
|
||||
<el-input
|
||||
v-model="form.refundRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入退款备注(可选)"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 退款总计 -->
|
||||
<el-divider content-position="left">退款总计</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<div class="refund-summary">
|
||||
<el-alert
|
||||
:title="`退款总计:¥${totalRefundAmount}`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<div slot="description">
|
||||
<p>金额退款:¥{{ form.moneyRefund || '0.00' }}</p>
|
||||
<p>余额退款:¥{{ form.balanceRefund || '0.00' }}</p>
|
||||
<p>优惠券返还:¥{{ form.couponRefund || '0.00' }}</p>
|
||||
<p>购物金返还:¥{{ form.shoppingRefund || '0.00' }}</p>
|
||||
</div>
|
||||
</el-alert>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm" :disabled="!canConfirm">确认退款</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "RefundAmountDialog",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
orderAmount: 0,
|
||||
payAmount: 0,
|
||||
balanceAmount: 0,
|
||||
couponAmount: 0,
|
||||
shoppingAmount: 0,
|
||||
moneyRefund: '',
|
||||
balanceRefund: '',
|
||||
couponRefund: '',
|
||||
shoppingRefund: '',
|
||||
refundRemark: '',
|
||||
enableMoneyRefund: false,
|
||||
enableBalanceRefund: false,
|
||||
enableCouponRefund: false,
|
||||
enableShoppingRefund: false
|
||||
})
|
||||
},
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/** 计算退款总金额 */
|
||||
totalRefundAmount() {
|
||||
const money = parseFloat(this.form.moneyRefund) || 0;
|
||||
const balance = parseFloat(this.form.balanceRefund) || 0;
|
||||
const coupon = parseFloat(this.form.couponRefund) || 0;
|
||||
const shopping = parseFloat(this.form.shoppingRefund) || 0;
|
||||
return (money + balance + coupon + shopping).toFixed(2);
|
||||
},
|
||||
/** 是否可以确认退款 */
|
||||
canConfirm() {
|
||||
return this.totalRefundAmount > 0;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCancel() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$refs.refundAmountForm.validate((valid) => {
|
||||
if (valid) {
|
||||
// 构建退款数据
|
||||
const refundData = {
|
||||
...this.form,
|
||||
totalRefundAmount: this.totalRefundAmount,
|
||||
refundDetails: {
|
||||
moneyRefund: parseFloat(this.form.moneyRefund) || 0,
|
||||
balanceRefund: parseFloat(this.form.balanceRefund) || 0,
|
||||
couponRefund: parseFloat(this.form.couponRefund) || 0,
|
||||
shoppingRefund: parseFloat(this.form.shoppingRefund) || 0
|
||||
}
|
||||
};
|
||||
this.$emit('confirm', refundData);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.refund-summary {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.refund-summary .el-alert__description p {
|
||||
margin: 5px 0;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="驳回理由"
|
||||
:visible.sync="localVisible"
|
||||
width="40%"
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="rejectReasonForm" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="驳回理由" prop="rejectReason">
|
||||
<el-input
|
||||
v-model="form.rejectReason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入驳回理由"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleConfirm">确认驳回</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "RejectReasonDialog",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
rejectReason: ''
|
||||
})
|
||||
},
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCancel() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$refs.rejectReasonForm.validate((valid) => {
|
||||
if (valid) {
|
||||
this.$emit('confirm', this.form);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="订单发货"
|
||||
:visible.sync="localVisible"
|
||||
width="900px"
|
||||
append-to-body
|
||||
>
|
||||
<!-- 主订单信息 -->
|
||||
<el-card shadow="hover" style="margin-bottom: 20px;" v-if="currentShipOrder && currentShipOrder.isMainRow">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #409EFF;">
|
||||
<i class="el-icon-s-order"></i>
|
||||
主订单信息
|
||||
</span>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="info-item">
|
||||
<label>主订单号:</label>
|
||||
<span class="value">{{ currentShipOrder.mainOrderId }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="info-item">
|
||||
<label>收货人:</label>
|
||||
<span class="value">{{ currentShipOrder.name }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="info-item">
|
||||
<label>联系电话:</label>
|
||||
<span class="value">{{ currentShipOrder.phone }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<div class="info-item">
|
||||
<label>收货地址:</label>
|
||||
<span class="value address">{{ currentShipOrder.address || '未设置' }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 发货信息 -->
|
||||
<el-card shadow="hover" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #67C23A;">
|
||||
<i class="el-icon-truck"></i>
|
||||
发货信息
|
||||
</span>
|
||||
</div>
|
||||
<el-form ref="shipForm" :model="shipForm" :rules="shipRules" label-width="120px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="快递公司" prop="deliveryId">
|
||||
|
||||
<el-select v-model="shipForm.deliveryId" placeholder="请选择快递公司" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in deliveryList"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="快递单号" prop="deliveryNum">
|
||||
<el-input v-model="shipForm.deliveryNum" placeholder="请输入快递单号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发货时间" prop="sendTime">
|
||||
<el-date-picker
|
||||
v-model="shipForm.sendTime"
|
||||
type="datetime"
|
||||
placeholder="请选择发货时间"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发货备注" prop="mark">
|
||||
<el-input v-model="shipForm.mark" placeholder="请输入发货备注(可选)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<el-card shadow="hover" style="margin-bottom: 20px;">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; color: #E6A23C;">
|
||||
<i class="el-icon-goods"></i>
|
||||
商品列表
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="shipmentOrders" border style="width: 100%">
|
||||
<!-- <el-table-column label="商品图片" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<div class="product-image">
|
||||
<el-image
|
||||
:src="getProductImage(scope.row)"
|
||||
style="width: 50px; height: 50px"
|
||||
fit="cover"
|
||||
:preview-src-list="[getProductImage(scope.row)]"
|
||||
>
|
||||
<div slot="error" class="image-slot">
|
||||
<i
|
||||
class="el-icon-picture-outline"
|
||||
style="font-size: 20px; color: #c0c4cc"
|
||||
></i>
|
||||
</div>
|
||||
</el-image>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column label="商品名称" prop="productName" min-width="200" />
|
||||
<el-table-column label="数量" prop="num" width="80" align="center" />
|
||||
<el-table-column label="规格" prop="sku" align="center">
|
||||
<template slot-scope="scope">
|
||||
|
||||
<div class="sku-detail">
|
||||
<template v-if="scope.row.sku && isJsonString(scope.row.sku)">
|
||||
<template v-for="(value, key) in getSkuData(scope.row.sku)">
|
||||
<div
|
||||
v-if="
|
||||
key !== 'price' && key !== 'stock' && key !== 'pic'
|
||||
"
|
||||
>
|
||||
{{ key }}:{{ value }}
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="sku-simple">{{ scope.row.sku || "-" }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" prop="totalPrice" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
¥{{ scope.row.totalPrice ? scope.row.totalPrice.toFixed(2) : '0.00' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ shipMode === 'batch' ? '批量发货' : '确认发货' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ShipmentDialog",
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
currentShipOrder: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
shipmentOrders: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
deliveryList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
shipForm: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
deliveryId: '',
|
||||
deliveryNum: '',
|
||||
sendTime: '',
|
||||
mark: ''
|
||||
})
|
||||
},
|
||||
shipRules: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
shipMode: {
|
||||
type: String,
|
||||
default: 'single'
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
shipmentOrders: {
|
||||
handler(newVal) {
|
||||
this.shipmentOrders = newVal;
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 判断字符串是否为有效的JSON */
|
||||
isJsonString(str) {
|
||||
if (!str || typeof str !== "string" || str.trim() === "") {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(str);
|
||||
return parsed && typeof parsed === "object";
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/** 解析SKU数据 */
|
||||
getSkuData(skuString) {
|
||||
if (!this.isJsonString(skuString)) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(skuString);
|
||||
} catch (e) {
|
||||
console.warn("SKU数据解析失败:", e);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
/** 获取商品图片,优先使用规格中的图片 */
|
||||
getProductImage(row) {
|
||||
// 优先使用规格中的图片
|
||||
if (row.sku && this.isJsonString(row.sku)) {
|
||||
try {
|
||||
const skuData = JSON.parse(row.sku);
|
||||
if (skuData.pic) {
|
||||
return skuData.pic;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("SKU数据解析失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 其次使用商品图片
|
||||
if (row.productImage) {
|
||||
return row.productImage;
|
||||
}
|
||||
|
||||
// 最后使用pic字段
|
||||
if (row.pic) {
|
||||
return row.pic;
|
||||
}
|
||||
|
||||
// 都没有则返回空字符串
|
||||
return '';
|
||||
},
|
||||
handleCancel() {
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$refs.shipForm.validate((valid) => {
|
||||
if (valid) {
|
||||
this.$emit('confirm', this.shipForm);
|
||||
}
|
||||
});
|
||||
},
|
||||
getStatusType(status) {
|
||||
const statusTypeMap = {
|
||||
1: 'info', // 待支付
|
||||
2: 'warning', // 已支付待发货
|
||||
3: 'success', // 已发货
|
||||
4: 'info', // 待评价
|
||||
5: 'success', // 已完成
|
||||
6: 'danger' // 已取消
|
||||
};
|
||||
return statusTypeMap[status] || 'info';
|
||||
},
|
||||
getStatusText(status) {
|
||||
const statusTextMap = {
|
||||
1: '待支付',
|
||||
2: '已支付待发货',
|
||||
3: '已发货',
|
||||
4: '待评价',
|
||||
5: '已完成',
|
||||
6: '已取消'
|
||||
};
|
||||
return statusTextMap[status] || '未知状态';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
color: #606266;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
color: #409EFF;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-item .value.address {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #f5f7fa;
|
||||
color: #c0c4cc;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,147 @@
|
|||
# 接单记录显示修复说明
|
||||
|
||||
## 问题描述
|
||||
|
||||
在订单处理对话框中,接单记录流程部分显示为"暂无接单记录",但实际上后端API返回了完整的接单记录数据。通过分析图片中的接单记录表格,发现数据结构与前端显示逻辑不匹配。
|
||||
|
||||
## 问题分析
|
||||
|
||||
### 1. 数据结构不匹配
|
||||
- **前端期望字段**:`status`、`workerName`、`createTime`等
|
||||
- **后端实际字段**:`title`、`content`、`workerName`、`createdAt`等
|
||||
- **时间字段差异**:前端使用`createTime`,后端返回`createdAt`
|
||||
|
||||
### 2. 状态映射错误
|
||||
- **前端状态判断**:基于数字状态码(1、2、3、4、5)
|
||||
- **后端实际数据**:基于文本标题("订单生成"、"开始服务"、"服务完成"等)
|
||||
|
||||
### 3. 字段映射问题
|
||||
- **师傅姓名**:前端期望`workerName`,后端正确返回
|
||||
- **处理内容**:前端期望`content`,后端正确返回
|
||||
- **时间显示**:前端期望`createTime`,后端返回`createdAt`
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 1. 调整字段映射
|
||||
```javascript
|
||||
// 修复前
|
||||
:timestamp="record.createTime"
|
||||
:type="getTimelineItemType(record.status)"
|
||||
|
||||
// 修复后
|
||||
:timestamp="formatTimelineTime(record.createdAt || record.addTime || record.createTime)"
|
||||
:type="getTimelineItemType(record.title)"
|
||||
```
|
||||
|
||||
### 2. 更新状态判断逻辑
|
||||
```javascript
|
||||
// 修复前:基于数字状态码
|
||||
getTimelineItemType(status) {
|
||||
switch (status) {
|
||||
case 1: return 'primary' // 接单
|
||||
case 2: return 'success' // 开始服务
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// 修复后:基于文本标题
|
||||
getTimelineItemType(title) {
|
||||
if (!title) return 'info'
|
||||
if (title.includes('订单生成') || title.includes('创建')) return 'primary'
|
||||
if (title.includes('支付成功') || title.includes('接单')) return 'success'
|
||||
if (title.includes('出发') || title.includes('到达')) return 'warning'
|
||||
if (title.includes('开始服务')) return 'info'
|
||||
if (title.includes('服务完成') || title.includes('完成')) return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 增强调试功能
|
||||
- 添加详细的console.log输出
|
||||
- 显示接单记录数量
|
||||
- 添加刷新按钮便于调试
|
||||
|
||||
### 4. 优化用户体验
|
||||
- 显示记录总数
|
||||
- 添加刷新接单记录按钮
|
||||
- 优化时间显示格式
|
||||
|
||||
## 修复后的功能特性
|
||||
|
||||
### 1. 正确的数据映射
|
||||
- **标题显示**:使用`record.title`显示状态标题
|
||||
- **内容显示**:使用`record.content`显示处理内容
|
||||
- **师傅信息**:使用`record.workerName`显示师傅姓名
|
||||
- **时间显示**:使用`record.createdAt`显示创建时间
|
||||
|
||||
### 2. 智能状态识别
|
||||
- **订单生成**:蓝色标识,表示订单创建
|
||||
- **支付成功**:绿色标识,表示订单确认
|
||||
- **出发/到达**:橙色标识,表示师傅行动
|
||||
- **开始服务**:灰色标识,表示服务进行
|
||||
- **服务完成**:红色标识,表示服务结束
|
||||
|
||||
### 3. 调试和监控
|
||||
- **API调用日志**:详细记录API请求和响应
|
||||
- **数据状态监控**:显示记录数量和加载状态
|
||||
- **手动刷新**:支持手动刷新接单记录
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 1. 前端修复
|
||||
- **Vue.js组件**:修复时间轴数据绑定
|
||||
- **Element UI**:优化Timeline组件显示
|
||||
- **数据格式化**:统一时间显示格式
|
||||
- **状态映射**:基于文本内容的状态判断
|
||||
|
||||
### 2. 后端API
|
||||
- **接口路径**:`/system/Order/receive-records/{orderId}`
|
||||
- **返回数据**:`OrderLog`对象列表
|
||||
- **数据字段**:`title`、`content`、`workerName`、`createdAt`
|
||||
|
||||
### 3. 数据流程
|
||||
1. 用户点击订单处理
|
||||
2. 调用`loadReceiveRecords`方法
|
||||
3. 请求后端API获取接单记录
|
||||
4. 解析返回的`OrderLog`数据
|
||||
5. 在时间轴中显示记录
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 1. 功能测试
|
||||
- [x] 接单记录正确加载
|
||||
- [x] 时间轴正确显示
|
||||
- [x] 状态颜色正确映射
|
||||
- [x] 师傅信息正确显示
|
||||
|
||||
### 2. 数据测试
|
||||
- [x] 订单号参数正确传递
|
||||
- [x] API响应正确解析
|
||||
- [x] 字段映射正确
|
||||
- [x] 时间格式正确
|
||||
|
||||
### 3. 用户体验测试
|
||||
- [x] 记录数量显示正确
|
||||
- [x] 刷新按钮功能正常
|
||||
- [x] 空数据提示正确
|
||||
- [x] 样式布局美观
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据一致性**:确保前端字段映射与后端数据结构一致
|
||||
2. **状态映射**:状态判断基于文本内容,需要维护关键词列表
|
||||
3. **时间格式**:统一使用`createdAt`字段,支持多种时间格式
|
||||
4. **错误处理**:API调用失败时显示友好提示
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. **状态配置化**:将状态关键词配置化,便于维护
|
||||
2. **实时更新**:添加WebSocket支持实时更新接单记录
|
||||
3. **数据缓存**:实现接单记录的数据缓存机制
|
||||
4. **导出功能**:支持导出接单记录为PDF或Excel
|
||||
|
||||
## 总结
|
||||
|
||||
通过修复字段映射、状态判断逻辑和增强调试功能,成功解决了接单记录显示为空的问题。现在时间轴能够正确显示完整的接单流程,包括订单生成、支付确认、师傅派单、服务执行到完成的整个过程。
|
||||
|
||||
修复后的功能提供了更好的用户体验和更准确的数据展示,让管理员能够清晰地了解每个订单的处理流程和状态变化。
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
# 订单处理功能增强说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
在原有订单处理功能的基础上,新增了完整的订单基本信息展示和接单记录流程展示功能,让管理员能够更全面地了解订单的详细信息和处理流程。
|
||||
|
||||
## 新增功能特性
|
||||
|
||||
### 1. 完整的订单基本信息展示
|
||||
- **订单标识信息**:订单号、订单状态、服务进度、下单时间
|
||||
- **用户信息**:用户姓名、用户电话
|
||||
- **服务信息**:服务名称、预约数量
|
||||
- **预约信息**:预约时间、预约地点
|
||||
- **价格信息**:订单总价、支付金额
|
||||
|
||||
### 2. 接单记录流程展示
|
||||
- **时间轴布局**:使用Element UI的Timeline组件展示接单记录
|
||||
- **状态标识**:不同状态用不同颜色和图标区分
|
||||
- **详细信息**:显示师傅信息、处理内容、备注、价格等
|
||||
- **流程追踪**:清晰展示订单从接单到完成的整个流程
|
||||
|
||||
### 3. 智能数据加载
|
||||
- **自动加载**:打开订单处理时自动加载接单记录
|
||||
- **数据格式化**:自动格式化时间、价格等数据
|
||||
- **错误处理**:API调用失败时的友好提示
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 前端实现
|
||||
- **Vue.js + Element UI**:使用Timeline组件展示流程
|
||||
- **响应式数据**:动态加载和显示接单记录
|
||||
- **数据格式化**:时间戳转换、状态文本映射
|
||||
- **样式优化**:美观的时间轴和卡片布局
|
||||
|
||||
### 后端API调用
|
||||
- **接单记录**:调用 `/system/Order/receive-records/{orderId}` 接口
|
||||
- **工人列表**:调用 `/system/Order/getWorkerList` 接口
|
||||
- **订单更新**:复用现有的 `updateOrder` 接口
|
||||
|
||||
### 数据字段映射
|
||||
```javascript
|
||||
// 订单基本信息
|
||||
userName: row.uname || row.name, // 用户姓名
|
||||
userPhone: row.phone || row.userPhone, // 用户电话
|
||||
productName: row.productName, // 服务名称
|
||||
num: row.num, // 预约数量
|
||||
appointmentTime: 格式化预约时间, // 预约时间
|
||||
appointmentAddress: row.address, // 预约地点
|
||||
totalPrice: row.totalPrice, // 订单总价
|
||||
payPrice: row.payPrice, // 支付金额
|
||||
createdAt: 格式化创建时间, // 下单时间
|
||||
|
||||
// 接单记录
|
||||
receiveRecords: [] // 接单记录数组
|
||||
```
|
||||
|
||||
## 接单记录状态映射
|
||||
|
||||
### 状态类型
|
||||
- **1 - 接单**:师傅接单,蓝色标识
|
||||
- **2 - 开始服务**:开始提供服务,绿色标识
|
||||
- **3 - 暂停服务**:服务暂停,橙色标识
|
||||
- **4 - 恢复服务**:服务恢复,灰色标识
|
||||
- **5 - 完成服务**:服务完成,红色标识
|
||||
|
||||
### 时间轴样式
|
||||
- **节点颜色**:根据状态自动设置不同颜色
|
||||
- **标签类型**:状态标签使用对应的Element UI类型
|
||||
- **卡片布局**:每个记录用卡片形式展示详细信息
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 查看订单基本信息
|
||||
- 打开订单处理对话框后,顶部会显示完整的订单基本信息
|
||||
- 所有信息字段都是只读的,用于查看和确认
|
||||
|
||||
### 2. 查看接单记录流程
|
||||
- 在订单基本信息下方,会显示接单记录的时间轴
|
||||
- 每个记录包含师傅信息、处理内容、时间等详细信息
|
||||
- 如果没有接单记录,会显示"暂无接单记录"的提示
|
||||
|
||||
### 3. 处理订单状态
|
||||
- 在订单基本信息区域可以修改订单状态和服务进度
|
||||
- 系统会根据状态变化自动验证必填字段
|
||||
- 修改完成后点击"确定"保存更改
|
||||
|
||||
## 样式特性
|
||||
|
||||
### 时间轴样式
|
||||
- **节点样式**:16x16像素的圆形节点,不同状态不同颜色
|
||||
- **卡片样式**:圆角边框,轻微阴影,清晰的信息层次
|
||||
- **响应式布局**:自适应不同屏幕尺寸
|
||||
|
||||
### 信息展示
|
||||
- **标签样式**:状态标签使用Element UI的Tag组件
|
||||
- **图标支持**:电话、金钱、定金等图标增强可读性
|
||||
- **颜色搭配**:合理的颜色搭配提升视觉体验
|
||||
|
||||
## 数据加载流程
|
||||
|
||||
1. **用户点击订单处理**:触发 `handleOrderProcess` 方法
|
||||
2. **初始化表单数据**:从行数据中提取基本信息并格式化
|
||||
3. **加载工人列表**:调用 `loadWorkerList` 获取可用工人
|
||||
4. **加载接单记录**:调用 `loadReceiveRecords` 获取接单历史
|
||||
5. **显示对话框**:展示完整的订单处理界面
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据完整性**:确保订单行数据包含所有必要字段
|
||||
2. **时间格式**:预约时间和创建时间会自动格式化显示
|
||||
3. **状态映射**:接单记录状态需要与后端保持一致
|
||||
4. **权限控制**:需要相应的查询和编辑权限
|
||||
5. **错误处理**:API调用失败时有友好的错误提示
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. **实时更新**:可以添加WebSocket支持实时更新接单记录
|
||||
2. **批量操作**:支持批量查看多个订单的处理流程
|
||||
3. **导出功能**:支持导出接单记录为PDF或Excel
|
||||
4. **通知系统**:状态变更时自动通知相关人员
|
||||
5. **移动端优化**:优化移动设备上的时间轴显示
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **数据加载测试**:测试各种订单数据的加载和显示
|
||||
2. **接单记录测试**:测试不同状态的接单记录显示
|
||||
3. **时间格式化测试**:测试各种时间格式的正确显示
|
||||
4. **样式兼容性测试**:测试不同浏览器的样式显示
|
||||
5. **响应式测试**:测试不同屏幕尺寸的布局效果
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
# 订单处理功能实现说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
在服务订单页面(`Order/index.vue`)添加了"订单处理"功能,允许管理员通过一个完整的对话框来管理订单的整个生命周期,包括状态变更、师傅分配、时间管理等。
|
||||
|
||||
## 主要特性
|
||||
|
||||
### 1. 订单处理按钮
|
||||
- 在页面顶部工具栏添加了"订单处理"按钮
|
||||
- 在每行操作列中添加了订单处理按钮(圆形图标)
|
||||
- 按钮只有在选中单行时才可用
|
||||
|
||||
### 2. 完整的订单处理对话框
|
||||
- **订单基本信息**:订单号、订单状态、服务进度
|
||||
- **师傅信息**:选择师傅、出发时间、到达时间
|
||||
- **服务时间**:开始时间、完成时间、下次服务时间
|
||||
- **暂停信息**:暂停时间、暂停原因(当服务进度为"已暂停"时显示)
|
||||
- **取消信息**:取消时间、取消原因(当订单状态为"已取消"或"未服务提前结束"时显示)
|
||||
- **备注信息**:备注字段
|
||||
|
||||
### 3. 智能状态管理
|
||||
- 根据订单状态变化自动设置相关时间字段
|
||||
- 状态变更时的业务逻辑验证(如选择师傅、填写原因等)
|
||||
- 服务进度变更时的智能处理
|
||||
|
||||
### 4. 后端API支持
|
||||
- 新增 `/system/Order/getWorkerList` 接口获取工人列表
|
||||
- 复用现有的 `updateOrder` 接口进行订单更新
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 前端实现
|
||||
- **Vue.js + Element UI**:使用卡片布局和表单组件
|
||||
- **响应式设计**:根据状态动态显示/隐藏相关字段
|
||||
- **表单验证**:集成现有的验证规则
|
||||
- **API调用**:异步获取工人列表,错误处理机制
|
||||
|
||||
### 后端实现
|
||||
- **OrderController.java**:新增获取工人列表接口
|
||||
- **权限控制**:使用 `@PreAuthorize` 注解
|
||||
- **数据过滤**:按用户类型筛选工人
|
||||
|
||||
### 状态流转逻辑
|
||||
1. **待接单 → 待服务**:需要选择师傅
|
||||
2. **待服务 → 服务中**:自动设置开始时间
|
||||
3. **服务中 → 已结束**:自动设置完成时间
|
||||
4. **服务中 → 已暂停**:需要填写暂停原因
|
||||
5. **已暂停 → 服务中**:自动设置开始时间
|
||||
6. **服务中 → 已取消**:需要填写取消原因
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 打开订单处理
|
||||
- 选择一行订单数据
|
||||
- 点击"订单处理"按钮
|
||||
- 或点击操作列中的订单处理图标
|
||||
|
||||
### 2. 修改订单状态
|
||||
- 选择新的订单状态
|
||||
- 系统会自动验证相关条件
|
||||
- 根据状态显示相应的必填字段
|
||||
|
||||
### 3. 分配师傅
|
||||
- 从下拉列表中选择师傅
|
||||
- 设置出发时间和到达时间
|
||||
|
||||
### 4. 管理服务时间
|
||||
- 设置服务开始和完成时间
|
||||
- 安排下次服务时间(如需要)
|
||||
|
||||
### 5. 处理特殊情况
|
||||
- **暂停服务**:填写暂停原因
|
||||
- **取消订单**:填写取消原因
|
||||
- **添加备注**:记录重要信息
|
||||
|
||||
### 6. 提交更改
|
||||
- 点击"确定"按钮保存更改
|
||||
- 系统会调用后端API更新订单
|
||||
- 成功后自动刷新订单列表
|
||||
|
||||
## 文件修改清单
|
||||
|
||||
### 前端文件
|
||||
- `ruoyi-ui/src/views/system/Order/index.vue`
|
||||
- 添加订单处理按钮
|
||||
- 实现订单处理对话框
|
||||
- 添加相关方法和数据
|
||||
- 集成CSS样式
|
||||
|
||||
### 后端文件
|
||||
- `ruoyi-system/src/main/java/com/ruoyi/system/controller/OrderController.java`
|
||||
- 新增 `getWorkerList` 接口
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限要求**:需要 `system:Order:edit` 权限
|
||||
2. **数据完整性**:状态变更时会自动验证必填字段
|
||||
3. **时间管理**:系统会自动设置相关时间戳
|
||||
4. **错误处理**:API调用失败时有友好的错误提示
|
||||
5. **响应式设计**:对话框会根据内容动态调整显示
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. **工作流引擎**:可以集成更复杂的状态机
|
||||
2. **通知系统**:状态变更时自动通知相关人员
|
||||
3. **日志记录**:记录所有状态变更的详细日志
|
||||
4. **批量处理**:支持批量订单状态变更
|
||||
5. **移动端适配**:优化移动设备的使用体验
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **功能测试**:测试各种状态变更场景
|
||||
2. **权限测试**:验证不同权限用户的访问控制
|
||||
3. **数据验证**:测试必填字段的验证逻辑
|
||||
4. **API测试**:验证后端接口的响应
|
||||
5. **UI测试**:测试对话框的显示和交互
|
||||
Loading…
Reference in New Issue