2025008071805

This commit is contained in:
张潘 2025-08-12 18:03:14 +08:00
parent 04a7dd0bd6
commit 8ae1dc51cf
25 changed files with 5200 additions and 2706 deletions

View File

@ -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直接获取最新数据
- ✅ **性能提升**:减少前端数据处理,提升响应速度
- ✅ **功能完善**:支持主订单的完整生命周期管理
- ✅ **用户体验**:直观的界面和流畅的操作流程
- ✅ **系统稳定**:完善的错误处理和降级方案
- ✅ **商品信息丰富**:显示完整的商品图片、分类、描述等信息
- ✅ **数据完整性**:确保订单和商品信息的一致性

View File

@ -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页面的新增订单功能实现了
- ✅ **完整的订单创建流程**:用户选择 → 商品选择 → 时间设置 → 地址选择 → 信息确认
- ✅ **智能的数据填充**:自动填充商品价格、用户地址等信息
- ✅ **完善的表单验证**:必填字段验证、格式验证、关联验证
- ✅ **友好的用户界面**:清晰的布局、直观的操作、实时的反馈
- ✅ **稳定的数据处理**:完善的错误处理、状态管理、异常处理
新增订单功能现在更加符合实际业务需求,为业务人员提供了便捷、高效、可靠的订单创建工具。

View File

@ -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. 可考虑支持按商品类型进行二次分组

View File

@ -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分组查询显示"的需求,同时大大提升了页面的功能性和用户体验。

View File

@ -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. 可以添加数据备份和恢复功能

View File

@ -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();
}
}
/**

View File

@ -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字符串

View File

@ -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";
/**
* 其他配置常量
*/

View File

@ -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>

View File

@ -177,7 +177,9 @@ aside {
margin-bottom: 10px;
}
}
.el-drawer__body {
padding: 20px !important;
}
/* 修复对话框遮罩层问题 */
//.v-modal {
// z-index: 9998 !important;

View File

@ -0,0 +1,3 @@
import AddressSelector from './index.vue'
export default AddressSelector

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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
## 总结
通过修复字段映射、状态判断逻辑和增强调试功能,成功解决了接单记录显示为空的问题。现在时间轴能够正确显示完整的接单流程,包括订单生成、支付确认、师傅派单、服务执行到完成的整个过程。
修复后的功能提供了更好的用户体验和更准确的数据展示,让管理员能够清晰地了解每个订单的处理流程和状态变化。

View File

@ -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. **响应式测试**:测试不同屏幕尺寸的布局效果

View File

@ -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测试**:测试对话框的显示和交互