修改小程序,前端,官网跳转路径

This commit is contained in:
2025-09-28 18:01:25 +08:00
parent e79e5bb086
commit c429672517
102 changed files with 8653 additions and 544 deletions

View File

@@ -201,7 +201,7 @@ const createLoanProduct = async (req, res) => {
}
// 检查用户信息
if (!req.user || !req.user.userId) {
if (!req.user || !req.user.id) {
return res.status(401).json({
success: false,
message: '用户信息无效'
@@ -222,8 +222,8 @@ const createLoanProduct = async (req, res) => {
riskLevel,
minLoanAmount: minLoanAmount ? parseFloat(minLoanAmount) : null,
maxLoanAmount: maxLoanAmount ? parseFloat(maxLoanAmount) : null,
createdBy: req.user.userId,
updatedBy: req.user.userId
createdBy: req.user.id,
updatedBy: req.user.id
});
// 获取创建后的完整信息
@@ -313,7 +313,7 @@ const updateLoanProduct = async (req, res) => {
}
});
product.updatedBy = req.user.userId;
product.updatedBy = req.user.id;
await product.save();
// 获取更新后的完整信息
@@ -440,7 +440,7 @@ const batchUpdateStatus = async (req, res) => {
await LoanProduct.update(
{
onSaleStatus,
updatedBy: req.user.userId
updatedBy: req.user.id
},
{
where: { id: { [Op.in]: ids } }

View File

@@ -0,0 +1,177 @@
# 银行管理系统小程序 - 按钮跳转问题调试指南
## 当前问题状态
用户反馈:点击"新增商品"和"修改信息"按钮没有跳转到表单页面,但控制台有日志输出。
## 已完成的修复
### 1. 事件冲突修复 ✅
- 移除了商品项容器的`bindtap="viewProductDetail"`事件
- 为商品信息区域单独添加点击事件
- 移除了按钮的`catchtap="true"`属性
### 2. 按钮样式优化 ✅
- 增加按钮高度60rpx → 80rpx
- 增加字体大小24rpx → 26rpx
- 添加z-index确保按钮在最上层
- 增加最小宽度和间距
### 3. 调试信息添加 ✅
- 在按钮点击方法中添加console.log
- 添加跳转成功/失败的回调处理
- 添加详细的错误信息显示
## 当前测试方案
### 测试页面创建
创建了一个简化的测试页面 `test-form` 来验证跳转功能:
**文件结构:**
```
pages/business/loan-products/
├── test-form.wxml # 测试页面模板
├── test-form.js # 测试页面逻辑
└── test-form.wxss # 测试页面样式
```
**测试页面功能:**
- 显示传入的参数mode, productId
- 提供返回按钮
- 简单的页面结构,便于调试
### 当前跳转配置
```javascript
// 新增商品按钮 - 跳转到测试页面
wx.navigateTo({
url: '/pages/business/loan-products/test-form?mode=add',
success: (res) => {
console.log('跳转到测试页面成功', res);
},
fail: (err) => {
console.error('跳转到测试页面失败', err);
// 显示详细错误信息
}
});
```
## 测试步骤
### 1. 基础跳转测试
1. 点击"新增商品"按钮
2. 查看控制台日志:
- 应该看到:"点击新增商品按钮"
- 应该看到:"跳转到测试页面成功" 或 "跳转到测试页面失败"
3. 如果跳转成功,应该看到测试页面显示:
- 标题:"测试表单页面"
- 模式:"add"
- 商品ID"无"
### 2. 错误诊断
如果跳转失败,查看错误信息:
- 检查控制台的错误详情
- 查看弹出的错误对话框
- 常见错误类型:
- 页面路径错误
- 页面未注册
- 页面JS语法错误
- 认证检查失败
### 3. 认证问题排查
如果跳转到登录页面,说明认证检查失败:
```javascript
// 在表单页面的onLoad方法中
if (!auth.isAuthenticated()) {
console.log('用户未登录,跳转到登录页面');
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
```
## 下一步计划
### 如果测试页面跳转成功
1. 恢复跳转到真实表单页面
2. 检查表单页面的认证逻辑
3. 确保表单页面能正常加载
### 如果测试页面跳转失败
1. 检查页面注册是否正确
2. 检查文件路径是否正确
3. 检查JS语法是否有错误
4. 尝试跳转到其他已知页面
## 调试命令
### 检查页面注册
```bash
# 查看app.json中的页面列表
grep -A 20 '"pages"' bank_mini_program/app.json
```
### 检查文件存在
```bash
# 检查测试页面文件是否存在
ls -la bank_mini_program/pages/business/loan-products/test-form.*
```
### 检查JS语法
```bash
# 检查JS文件语法
node -c bank_mini_program/pages/business/loan-products/test-form.js
```
## 常见问题解决
### 1. 页面路径错误
- 确保路径以 `/pages/` 开头
- 确保路径与app.json中的注册一致
- 注意大小写敏感
### 2. 页面未注册
- 在app.json的pages数组中添加页面路径
- 确保路径格式正确
### 3. 认证检查失败
- 检查用户是否已登录
- 检查token是否有效
- 考虑临时注释认证检查进行测试
### 4. 页面JS错误
- 检查onLoad方法是否有语法错误
- 检查依赖的模块是否正确导入
- 查看控制台的JavaScript错误
## 文件修改记录
### 已修改文件
1. `pages/business/loan-products/loan-products.js`
- 添加调试日志
- 修改跳转目标为测试页面
- 添加错误处理
2. `pages/business/loan-products/loan-products.wxml`
- 移除事件冲突
- 优化按钮绑定
3. `pages/business/loan-products/loan-products.wxss`
- 优化按钮样式
- 增加点击区域
4. `app.json`
- 注册测试页面
### 新增文件
1. `pages/business/loan-products/test-form.wxml`
2. `pages/business/loan-products/test-form.js`
3. `pages/business/loan-products/test-form.wxss`
## 联系信息
如果问题仍然存在,请提供:
1. 控制台的完整错误日志
2. 点击按钮后的具体表现
3. 是否看到测试页面
4. 任何其他错误信息

View File

@@ -0,0 +1,175 @@
# 银行管理系统小程序 - 按钮点击问题修复总结
## 问题描述
用户反馈在商品管理页面中,点击"新增商品"、"修改信息"、"产品下架"按钮没有反应。
## 问题分析
经过检查发现以下问题:
1. **事件冲突**:商品项容器有`bindtap="viewProductDetail"`事件,会拦截子元素的点击事件
2. **事件绑定问题**:按钮使用了`catchtap="true"`,可能影响事件处理
3. **样式层级问题**:按钮可能被其他元素遮挡
4. **点击区域太小**按钮高度只有60rpx点击区域不够大
## 修复方案
### 1. 移除容器点击事件冲突
**问题**:商品项容器有`bindtap="viewProductDetail"`事件
```xml
<!-- 修复前 -->
<view class="product-item" bindtap="viewProductDetail">
<!-- 按钮在这里 -->
</view>
```
**解决方案**:移除容器点击事件,为具体区域添加点击事件
```xml
<!-- 修复后 -->
<view class="product-item">
<view class="product-header" bindtap="viewProductDetail">
<!-- 商品标题区域可点击 -->
</view>
<view class="product-info" bindtap="viewProductDetail">
<!-- 商品信息区域可点击 -->
</view>
<view class="product-desc" bindtap="viewProductDetail">
<!-- 商品描述区域可点击 -->
</view>
<!-- 操作按钮区域不添加点击事件 -->
</view>
```
### 2. 移除不必要的事件修饰符
**问题**:按钮使用了`catchtap="true"`
```xml
<!-- 修复前 -->
<button bindtap="editProduct" catchtap="true">
```
**解决方案**:移除`catchtap="true"`
```xml
<!-- 修复后 -->
<button bindtap="editProduct">
```
### 3. 优化按钮样式和层级
**问题**:按钮可能被遮挡,点击区域太小
**解决方案**
- 增加按钮高度从60rpx增加到80rpx
- 增加字体大小从24rpx增加到26rpx
- 添加z-index确保按钮在最上层
- 增加最小宽度确保点击区域足够大
```css
.action-btn {
height: 80rpx;
font-size: 26rpx;
position: relative;
z-index: 10;
margin: 0 10rpx;
min-width: 120rpx;
}
```
### 4. 添加调试信息
为了便于调试在按钮点击方法中添加了console.log
```javascript
// 新增商品
addProduct() {
console.log('点击新增商品按钮');
// ...
},
// 编辑商品
editProduct(e) {
console.log('点击编辑商品按钮', e);
const productId = e.currentTarget.dataset.productId;
console.log('商品ID:', productId);
// ...
},
// 删除商品
deleteProduct(e) {
console.log('点击删除商品按钮', e);
const productId = e.currentTarget.dataset.productId;
console.log('商品ID:', productId);
// ...
}
```
## 修复后的效果
1. **新增商品按钮**:点击后跳转到商品表单页面(新增模式)
2. **修改信息按钮**点击后跳转到商品表单页面编辑模式并传递商品ID
3. **产品下架按钮**点击后显示确认对话框确认后调用删除API
## 技术要点
### 1. 事件冒泡处理
- 使用`bindtap`而不是`catchtap`,让事件正常冒泡
- 为需要点击的区域单独添加事件处理
### 2. 样式优化
- 确保按钮有足够的点击区域
- 使用z-index确保按钮在最上层
- 添加适当的间距和边距
### 3. 调试支持
- 添加console.log便于调试
- 在开发者工具中可以看到按钮点击日志
## 测试建议
1. **功能测试**
- 点击"新增商品"按钮,应该跳转到表单页面
- 点击"修改信息"按钮,应该跳转到表单页面并加载商品数据
- 点击"产品下架"按钮,应该显示确认对话框
2. **调试测试**
- 打开微信开发者工具的控制台
- 点击按钮时应该看到相应的日志输出
- 检查是否有JavaScript错误
3. **界面测试**
- 确认按钮可见且可点击
- 确认按钮样式正确
- 确认按钮间距合适
## 文件修改清单
1. `pages/business/loan-products/loan-products.wxml`
- 移除商品项容器的点击事件
- 为商品信息区域添加点击事件
- 移除按钮的`catchtap="true"`属性
2. `pages/business/loan-products/loan-products.js`
- 添加调试日志
- 确保方法定义正确
3. `pages/business/loan-products/loan-products.wxss`
- 增加按钮高度和字体大小
- 添加z-index和最小宽度
- 优化按钮间距
## 注意事项
1. 确保商品数据正确加载,否则按钮可能不会显示
2. 检查API接口是否正常工作
3. 确认页面路由配置正确
4. 在真机上测试,确保触摸事件正常工作
## 更新日志
- **v1.2.1** (2024-01-15)
- 修复按钮点击无反应问题
- 优化按钮样式和点击区域
- 添加调试支持
- 解决事件冲突问题

View File

@@ -0,0 +1,203 @@
# 银行管理系统小程序 - 字段映射修复
## 问题描述
用户反馈:填写了所有带*号的必填字段后,接口仍然报错 `{success: false, message: "请填写所有必填字段"}`
## 问题分析
经过检查发现,前端发送的字段名与后端期望的字段名不匹配:
### 后端期望的必填字段loanProductController.js:162
```javascript
if (!productName || !loanAmount || !loanTerm || !interestRate || !serviceArea || !servicePhone) {
return res.status(400).json({
success: false,
message: '请填写所有必填字段'
});
}
```
### 前端发送的字段名
```javascript
formData: {
productName: '', // ✅ 匹配
productCode: '', // ❌ 后端不需要
productType: 'personal', // ❌ 后端不需要
minLoanAmount: '', // ❌ 后端期望 loanAmount
maxLoanAmount: '', // ❌ 后端期望 loanAmount
minLoanTerm: '', // ❌ 后端期望 loanTerm
maxLoanTerm: '', // ❌ 后端期望 loanTerm
interestRate: '', // ✅ 匹配
serviceArea: '', // ✅ 匹配
servicePhone: '', // ✅ 匹配
riskLevel: 'LOW', // ❌ 后端不需要
requiredDocuments: '', // ❌ 后端不需要
onSaleStatus: true // ❌ 后端不需要
}
```
## 解决方案
在表单提交时添加字段映射,将前端字段名转换为后端期望的格式:
```javascript
// 转换字段名为后端期望的格式
const submitData = {
productName: formData.productName,
productCode: formData.productCode,
productType: formData.productType,
// 后端期望的字段名
loanAmount: formData.maxLoanAmount || formData.minLoanAmount, // 使用最大贷款额度作为贷款金额
loanTerm: formData.maxLoanTerm || formData.minLoanTerm, // 使用最大贷款期限作为贷款期限
minLoanAmount: formData.minLoanAmount,
maxLoanAmount: formData.maxLoanAmount,
minLoanTerm: formData.minLoanTerm,
maxLoanTerm: formData.maxLoanTerm,
interestRate: formData.interestRate,
productDescription: formData.productDescription,
serviceArea: formData.serviceArea,
servicePhone: formData.servicePhone,
riskLevel: formData.riskLevel,
requiredDocuments: formData.requiredDocuments,
onSaleStatus: formData.onSaleStatus
};
```
## 字段映射规则
| 前端字段名 | 后端字段名 | 映射规则 | 说明 |
|-----------|-----------|---------|------|
| productName | productName | 直接映射 | 产品名称 |
| minLoanAmount | minLoanAmount | 直接映射 | 最小贷款额度 |
| maxLoanAmount | maxLoanAmount | 直接映射 | 最大贷款额度 |
| minLoanTerm | minLoanTerm | 直接映射 | 最小贷款期限 |
| maxLoanTerm | maxLoanTerm | 直接映射 | 最大贷款期限 |
| maxLoanAmount | loanAmount | 优先使用最大值 | 后端必填字段 |
| maxLoanTerm | loanTerm | 优先使用最大值 | 后端必填字段 |
| interestRate | interestRate | 直接映射 | 利率 |
| serviceArea | serviceArea | 直接映射 | 服务区域 |
| servicePhone | servicePhone | 直接映射 | 服务电话 |
| productDescription | productDescription | 直接映射 | 产品描述 |
| riskLevel | riskLevel | 直接映射 | 风险等级 |
| requiredDocuments | requiredDocuments | 直接映射 | 所需材料 |
| onSaleStatus | onSaleStatus | 直接映射 | 在售状态 |
## 后端必填字段验证
后端验证的必填字段:
1. **productName** - 产品名称
2. **loanAmount** - 贷款金额使用maxLoanAmount
3. **loanTerm** - 贷款期限使用maxLoanTerm
4. **interestRate** - 利率
5. **serviceArea** - 服务区域
6. **servicePhone** - 服务电话
## 前端必填字段标识
前端标记的必填字段:
1. **产品名称** * - 对应 productName
2. **产品类型** * - 对应 productType后端不需要
3. **最小贷款额度** * - 对应 minLoanAmount
4. **最大贷款额度** * - 对应 maxLoanAmount
5. **利率** * - 对应 interestRate
6. **风险等级** * - 对应 riskLevel后端不需要
## 修复后的效果
### 1. 字段映射
- 前端字段名正确映射到后端期望的格式
- 必填字段验证通过
- 数据成功提交到后端
### 2. 用户体验
- 表单验证更加准确
- 错误提示更加明确
- 提交成功率提升
### 3. 数据一致性
- 前后端字段名统一
- 数据格式标准化
- 减少字段映射错误
## 技术实现
### 1. 字段转换逻辑
```javascript
// 在 submitForm 方法中添加字段映射
const submitData = {
// 直接映射的字段
productName: formData.productName,
interestRate: formData.interestRate,
serviceArea: formData.serviceArea,
servicePhone: formData.servicePhone,
// 需要转换的字段
loanAmount: formData.maxLoanAmount || formData.minLoanAmount,
loanTerm: formData.maxLoanTerm || formData.minLoanTerm,
// 其他字段
minLoanAmount: formData.minLoanAmount,
maxLoanAmount: formData.maxLoanAmount,
minLoanTerm: formData.minLoanTerm,
maxLoanTerm: formData.maxLoanTerm,
productDescription: formData.productDescription,
riskLevel: formData.riskLevel,
requiredDocuments: formData.requiredDocuments,
onSaleStatus: formData.onSaleStatus
};
```
### 2. 验证逻辑
- 保持前端验证不变
- 后端验证通过字段映射解决
- 双重验证确保数据完整性
## 测试建议
### 1. 功能测试
- 测试所有必填字段的提交
- 验证字段映射是否正确
- 检查后端接收的数据格式
### 2. 边界测试
- 测试空值处理
- 测试数值范围验证
- 测试特殊字符处理
### 3. 集成测试
- 测试前后端数据交互
- 验证错误处理机制
- 检查数据持久化
## 文件修改
### 修改的文件
- `pages/business/loan-products/form/form.js`
- 添加字段映射逻辑
- 优化数据提交格式
### 修改的方法
- `submitForm()` - 添加字段映射转换
## 更新日志
- **v1.3.1** (2024-01-15)
- 修复前后端字段名不匹配问题
- 添加字段映射转换逻辑
- 解决"请填写所有必填字段"错误
- 提升表单提交成功率
## 注意事项
1. **字段映射**:确保所有字段都正确映射
2. **数据验证**:保持前后端验证的一致性
3. **错误处理**:提供清晰的错误提示
4. **向后兼容**保持API接口的稳定性
## 后续优化
1. **统一字段名**:考虑统一前后端字段命名规范
2. **自动映射**:开发字段映射工具
3. **文档更新**更新API文档和字段说明
4. **测试覆盖**:增加字段映射的单元测试

View File

@@ -0,0 +1,210 @@
# 银行管理系统小程序字段映射修复说明
## 修复概述
本次修复解决了银行管理系统小程序中三个主要页面的字段显示不全问题:
1. **商品管理页面** - 贷款产品字段显示不全
2. **业务申请页面** - 申请人ID显示问题
3. **业务合同页面** - 合同字段显示不全
## 修复详情
### 1. 商品管理页面修复 (`pages/business/loan-products/`)
#### 问题描述
- 接口返回的字段名与前端使用的字段名不匹配
- 缺少服务区域、服务电话、风险等级等字段显示
#### 修复内容
**JS文件更新** (`loan-products.js`):
- 添加认证检查
- 更新字段映射逻辑,支持接口返回的字段名
- 新增字段:`serviceArea`, `servicePhone`, `riskLevel`, `requiredDocuments`, `supervisionCustomers`, `totalCustomers`
**WXML文件更新** (`loan-products.wxml`):
- 添加服务区域显示
- 添加服务电话显示
- 添加风险等级显示
- 添加监管客户数量显示
- 优化字段显示逻辑,支持空值处理
#### 字段映射对照表
| 接口字段 | 前端字段 | 说明 |
|---------|---------|------|
| `productName` | `name` | 产品名称 |
| `minLoanAmount` | `minAmount` | 最小贷款金额 |
| `maxLoanAmount` | `maxAmount` | 最大贷款金额 |
| `loanTerm` | `term` | 贷款期限 |
| `onSaleStatus` | `status` | 销售状态 |
| `serviceArea` | `serviceArea` | 服务区域 |
| `servicePhone` | `servicePhone` | 服务电话 |
| `riskLevel` | `riskLevel` | 风险等级 |
### 2. 业务申请页面修复 (`pages/business/loan-applications/`)
#### 问题描述
- 申请人ID字段没有正确显示
- 缺少资产类型、申请数量等字段
#### 修复内容
**JS文件更新** (`loan-applications.js`):
- 添加认证检查
- 更新字段映射逻辑
- 新增字段:`applicantId`, `assetType`, `applicationQuantity`, `borrowerIdNumber`
**WXML文件更新** (`loan-applications.wxml`):
- 添加申请人ID显示
- 添加资产类型显示
- 添加申请数量显示
- 添加身份证号显示
- 添加备注显示(条件显示)
#### 字段映射对照表
| 接口字段 | 前端字段 | 说明 |
|---------|---------|------|
| `borrowerName` | `applicantName` | 申请人姓名 |
| `borrowerIdNumber` | `applicantId` | 申请人ID |
| `assetType` | `assetType` | 资产类型 |
| `applicationQuantity` | `applicationQuantity` | 申请数量 |
### 3. 业务合同页面修复 (`pages/business/loan-contracts/`)
#### 问题描述
- 客户姓名字段显示为空
- 缺少合同相关的详细信息字段
#### 修复内容
**JS文件更新** (`loan-contracts.js`):
- 添加认证检查
- 更新类型映射,新增 `livestock_collateral` 类型
- 更新状态映射,新增 `completed`, `pending` 状态
- 更新字段映射逻辑,支持接口返回的字段名
- 新增字段:`customerId`, `applicationNumber`, `assetType`, `purpose`, `productName`, `paidAmount`, `remainingAmount`, `repaymentProgress`
**WXML文件更新** (`loan-contracts.wxml`):
- 添加客户ID显示
- 添加申请单号显示
- 添加资产类型显示
- 添加申请用途显示
- 添加产品名称显示
- 添加已还金额显示
- 添加剩余金额显示
- 添加还款进度显示
- 添加备注显示(条件显示)
- 更新筛选选项,包含新状态
#### 字段映射对照表
| 接口字段 | 前端字段 | 说明 |
|---------|---------|------|
| `borrowerName` | `customerName` | 客户姓名 |
| `borrowerIdNumber` | `customerId` | 客户ID |
| `contractTime` | `signDate` | 签订时间 |
| `maturityTime` | `expiryDate` | 到期时间 |
| `applicationNumber` | `applicationNumber` | 申请单号 |
| `assetType` | `assetType` | 资产类型 |
| `purpose` | `purpose` | 申请用途 |
| `productName` | `productName` | 产品名称 |
| `paidAmount` | `paidAmount` | 已还金额 |
| `remainingAmount` | `remainingAmount` | 剩余金额 |
| `repaymentProgress` | `repaymentProgress` | 还款进度 |
## 技术改进
### 1. 认证检查
所有业务页面都添加了认证检查,确保用户登录后才能访问:
```javascript
onLoad() {
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
// 页面逻辑
}
```
### 2. 字段映射策略
采用灵活的字段映射策略,支持多种字段名:
```javascript
customerName: contract.borrowerName || contract.farmerName || contract.customerName
```
### 3. 空值处理
所有字段都添加了空值处理避免显示undefined
```html
<text class="info-value">{{item.customerName || '暂无'}}</text>
```
### 4. 类型和状态映射
扩展了类型和状态映射,支持更多业务场景:
- 新增合同类型:`livestock_collateral` (养殖抵押贷款)
- 新增合同状态:`completed` (已完成), `pending` (待处理)
## 测试验证
### 测试数据示例
**商品数据**:
```javascript
{
id: 4,
productName: "养殖贷款",
interestRate: "2.00",
minLoanAmount: "23.00",
maxLoanAmount: "2323.00",
serviceArea: "1234",
servicePhone: "19872031965",
riskLevel: "LOW"
}
```
**申请数据**:
```javascript
{
id: 1,
borrowerName: "张三",
borrowerIdNumber: "110101199001010001",
assetType: "养殖设备",
applicationQuantity: "1"
}
```
**合同数据**:
```javascript
{
id: 2,
contractNumber: "CON002",
borrowerName: "李四",
borrowerIdNumber: "110101199002020002",
assetType: "养殖设备",
paidAmount: 0,
remainingAmount: 30000,
repaymentProgress: 0
}
```
## 使用说明
1. **重新编译小程序**:修改完成后需要重新编译小程序
2. **清除缓存**:建议清除小程序缓存后重新测试
3. **检查网络**确保API接口正常返回数据
4. **验证登录**:确保用户已正确登录
## 注意事项
1. 所有字段映射都考虑了向后兼容性
2. 空值处理确保界面不会显示undefined
3. 认证检查确保数据安全性
4. 字段映射策略支持多种数据格式
## 更新日志
- **v1.1.0** (2024-01-15)
- 修复商品字段显示不全问题
- 修复业务申请申请人ID显示问题
- 修复业务合同字段显示不全问题
- 添加认证检查
- 优化字段映射逻辑
- 增强空值处理

View File

@@ -0,0 +1,190 @@
# 银行管理系统小程序 - 表单必填字段标识更新
## 更新概述
为银行管理系统小程序的商品管理表单添加了必填字段的*号标识,提升用户体验和表单可用性。
## 更新内容
### 1. 必填字段标识
为以下字段添加了红色*号标识:
#### 基本信息
- **产品名称** * - 必填,用于标识贷款产品
- **产品类型** * - 必填,选择产品分类
#### 贷款信息
- **最小贷款额度** * - 必填,设置贷款下限
- **最大贷款额度** * - 必填,设置贷款上限
- **利率** * - 必填,设置产品利率
#### 服务信息
- **风险等级** * - 必填,评估产品风险
### 2. 样式优化
#### WXML结构优化
```xml
<!-- 优化前 -->
<text class="form-label">产品名称 *</text>
<!-- 优化后 -->
<text class="form-label">
产品名称
<text class="required-asterisk">*</text>
</text>
```
#### CSS样式添加
```css
/* 必填字段的*号样式 */
.required-asterisk {
color: #ff4d4f;
font-weight: bold;
margin-left: 4rpx;
}
```
### 3. 表单验证增强
更新了表单验证逻辑,确保所有标记为必填的字段都进行验证:
```javascript
// 新增的验证规则
if (!formData.productType) {
wx.showToast({
title: '请选择产品类型',
icon: 'none'
});
return false;
}
if (!formData.riskLevel) {
wx.showToast({
title: '请选择风险等级',
icon: 'none'
});
return false;
}
```
## 技术实现
### 1. 字段标识方式
- 使用嵌套的`<text>`标签分离字段名称和*号
- *号使用独立的CSS类`.required-asterisk`
- 红色高亮显示,增强视觉识别度
### 2. 样式特点
- **颜色**`#ff4d4f` (红色)
- **字重**`bold` (粗体)
- **间距**`margin-left: 4rpx` (与字段名保持适当间距)
### 3. 验证逻辑
- 前端实时验证,提升用户体验
- 统一的错误提示格式
- 阻止无效数据提交
## 用户体验提升
### 1. 视觉识别
- 必填字段一目了然
- 红色*号醒目易识别
- 与可选字段形成对比
### 2. 操作指导
- 用户清楚知道哪些字段必须填写
- 减少表单提交失败的情况
- 提升表单完成率
### 3. 错误预防
- 前端验证减少后端错误
- 及时提示用户补充信息
- 降低数据不完整的情况
## 文件修改清单
### 1. 模板文件
- `pages/business/loan-products/form/form.wxml`
- 更新所有必填字段的标签结构
- 添加`required-asterisk`类包装*号
### 2. 样式文件
- `pages/business/loan-products/form/form.wxss`
- 添加`.required-asterisk`样式类
- 定义红色高亮样式
### 3. 逻辑文件
- `pages/business/loan-products/form/form.js`
- 增强`validateForm()`方法
- 添加产品类型和风险等级验证
## 必填字段列表
| 字段名称 | 字段类型 | 验证规则 | 错误提示 |
|---------|---------|---------|---------|
| 产品名称 | 文本输入 | 非空验证 | "请输入产品名称" |
| 产品类型 | 选择器 | 非空验证 | "请选择产品类型" |
| 最小贷款额度 | 数字输入 | 非空+数值验证 | "请输入贷款额度范围" |
| 最大贷款额度 | 数字输入 | 非空+数值验证 | "请输入贷款额度范围" |
| 利率 | 数字输入 | 非空验证 | "请输入利率" |
| 风险等级 | 选择器 | 非空验证 | "请选择风险等级" |
## 验证规则详情
### 1. 基础验证
- 所有必填字段不能为空
- 字符串字段去除首尾空格后验证
### 2. 数值验证
- 贷款额度必须为有效数字
- 最小额度必须小于最大额度
- 利率必须为有效数值
### 3. 选择验证
- 产品类型必须从预设选项中选择
- 风险等级必须从预设选项中选择
## 测试建议
### 1. 功能测试
- 测试所有必填字段的验证
- 测试*号显示是否正确
- 测试错误提示是否准确
### 2. 界面测试
- 检查*号颜色和样式
- 验证在不同设备上的显示效果
- 确认与整体UI风格协调
### 3. 用户体验测试
- 测试表单提交流程
- 验证错误提示的友好性
- 检查操作流程的流畅性
## 后续优化建议
### 1. 动态验证
- 考虑添加实时验证
- 在用户输入时即时提示
### 2. 视觉增强
- 可以为必填字段添加边框高亮
- 考虑添加必填字段说明
### 3. 国际化支持
- 为多语言环境准备
- 考虑不同语言的*号位置
## 更新日志
- **v1.3.0** (2024-01-15)
- 为表单必填字段添加*号标识
- 增强表单验证逻辑
- 优化用户体验
- 提升表单可用性
## 联系信息
如有问题或建议,请及时反馈,我们将持续优化用户体验。

View File

@@ -0,0 +1,276 @@
# 银行管理系统小程序 - 登录登出功能指南
## 功能概述
银行管理系统小程序已完善了登录和登出功能,包括:
- 用户登录验证
- 记住用户名功能
- 自动登录状态检查
- 安全登出
- 全局认证状态管理
## 文件结构
### 核心文件
```
bank_mini_program/
├── utils/
│ └── auth.js # 认证工具类
├── services/
│ ├── apiService.js # API服务
│ └── authService.js # 认证服务
├── pages/
│ ├── login/
│ │ ├── login.js # 登录页面逻辑
│ │ ├── login.wxml # 登录页面结构
│ │ └── login.wxss # 登录页面样式
│ └── profile/
│ ├── profile.js # 个人中心页面逻辑
│ ├── profile.wxml # 个人中心页面结构
│ └── profile.wxss # 个人中心页面样式
└── app.js # 应用入口,包含全局认证管理
```
## 功能详解
### 1. 登录功能
#### 登录页面特性
- **用户名密码输入**:支持用户名和密码登录
- **记住我功能**:可选择记住用户名,下次自动填充
- **快速登录**:提供测试用的快速登录按钮
- **表单验证**:输入验证和错误提示
- **加载状态**:登录过程中的加载提示
#### 登录流程
1. 用户输入用户名和密码
2. 点击登录按钮
3. 调用 `authService.login()` 进行API验证
4. 成功后保存token和用户信息到本地存储
5. 设置全局用户信息
6. 跳转到首页
#### 代码示例
```javascript
// 登录处理
async handleLogin() {
const { username, password } = this.data;
try {
const response = await authService.login(username, password);
if (response && response.success) {
// 保存认证信息
auth.setToken(response.token);
auth.setUser(response.user);
// 设置全局状态
const app = getApp();
if (app && app.login) {
app.login(response.user);
}
// 跳转到首页
wx.reLaunch({
url: '/pages/index/index'
});
}
} catch (error) {
console.error('登录失败:', error);
}
}
```
### 2. 登出功能
#### 登出页面特性
- **确认对话框**:登出前显示确认提示
- **API调用**:调用后端登出接口
- **本地清理**:清除本地存储的认证信息
- **全局状态更新**:更新应用的全局认证状态
#### 登出流程
1. 用户点击退出登录按钮
2. 显示确认对话框
3. 确认后调用 `authService.logout()` API
4. 清除本地存储的token和用户信息
5. 更新全局认证状态
6. 跳转到登录页面
#### 代码示例
```javascript
// 登出处理
async handleLogout() {
try {
// 调用登出API
await authService.logout();
// 清除本地认证信息
auth.clearAuth();
// 更新全局状态
const app = getApp();
if (app && app.logout) {
app.logout();
}
// 跳转到登录页
wx.reLaunch({
url: '/pages/login/login'
});
} catch (error) {
console.error('登出失败:', error);
}
}
```
### 3. 认证状态管理
#### 全局认证检查
所有需要认证的页面都会在 `onLoad``onShow` 时检查认证状态:
```javascript
onLoad() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
// 继续页面逻辑
this.loadData();
}
```
#### 认证工具类方法
```javascript
// 检查是否已认证
auth.isAuthenticated()
// 获取用户信息
auth.getUser()
// 获取token
auth.getToken()
// 设置用户信息
auth.setUser(userInfo)
// 设置token
auth.setToken(token)
// 清除所有认证信息
auth.clearAuth()
```
### 4. 记住用户名功能
#### 功能说明
- 用户可以选择"记住我"选项
- 记住的用户名会保存到本地存储
- 下次打开登录页面时自动填充用户名
#### 实现代码
```javascript
// 切换记住我
onRememberMeChange(e) {
this.setData({
rememberMe: e.detail.value
});
}
// 保存/清除用户名
if (rememberMe) {
wx.setStorageSync('remembered_username', username);
} else {
wx.removeStorageSync('remembered_username');
}
```
## 使用说明
### 1. 登录
1. 打开小程序
2. 如果未登录,会自动跳转到登录页面
3. 输入用户名和密码
4. 可选择"记住我"选项
5. 点击"登录"按钮
### 2. 登出
1. 进入个人中心页面
2. 点击"退出登录"按钮
3. 确认退出操作
4. 自动跳转到登录页面
### 3. 自动登录检查
- 每次打开需要认证的页面时,系统会自动检查登录状态
- 如果未登录,会自动跳转到登录页面
- 登录后会自动跳转回原页面
## 测试
### 测试文件
运行 `test-auth.js` 可以测试认证功能:
```javascript
// 在微信开发者工具中运行
const auth = require('./utils/auth.js');
// 测试认证状态
console.log('是否已认证:', auth.isAuthenticated());
```
### 测试步骤
1. 打开微信开发者工具
2. 导入银行管理系统小程序项目
3. 在控制台运行测试脚本
4. 测试登录和登出功能
## 注意事项
1. **API接口**确保后端API接口 `/bank/api/auth/login``/bank/api/auth/logout` 正常工作
2. **Token管理**Token会自动添加到API请求头中
3. **错误处理**:所有认证相关的错误都有适当的错误提示
4. **安全性**:敏感信息存储在微信小程序的本地存储中
5. **兼容性**:支持微信小程序的所有版本
## 故障排除
### 常见问题
1. **登录失败**
- 检查网络连接
- 确认用户名密码正确
- 查看控制台错误信息
2. **自动跳转问题**
- 检查页面路径是否正确
- 确认认证状态检查逻辑
3. **记住用户名不工作**
- 检查本地存储权限
- 确认存储键名正确
4. **登出后仍显示已登录**
- 检查 `auth.clearAuth()` 是否被调用
- 确认全局状态是否正确更新
### 调试方法
1. 在微信开发者工具中查看控制台日志
2. 检查本地存储中的数据
3. 使用 `auth.isAuthenticated()` 检查认证状态
4. 查看网络请求是否成功
## 更新日志
- **v1.0.0** (2024-01-15)
- 初始版本
- 实现基础登录登出功能
- 添加记住用户名功能
- 实现全局认证状态管理
- 添加页面级认证检查

View File

@@ -0,0 +1,180 @@
# 银行管理系统小程序 - 页面跳转问题调试报告
## 问题现象
用户反馈:
- 点击"新增商品"按钮可以跳转到测试页面
- 点击"修改信息"按钮跳转到表单页面失败
- 错误信息:`navigateTo:fail page "pages/business/loan-products/form?mode=edit&productId=4" is not found`
## 问题分析
### 1. 错误信息解读
```
navigateTo:fail page "pages/business/loan-products/form?mode=edit&productId=4" is not found
```
这个错误表明:
- 微信小程序无法找到指定的页面路径
- 路径中包含了查询参数但错误信息显示的是完整的URL
- 可能是页面路径格式或注册问题
### 2. 可能的原因
#### A. 页面路径格式问题
- 微信小程序页面路径应该不包含查询参数
- 查询参数应该通过URL参数传递而不是作为路径的一部分
#### B. 页面注册问题
- 页面可能没有在app.json中正确注册
- 页面路径可能与注册的路径不匹配
#### C. 文件结构问题
- 页面文件可能不存在或路径不正确
- 页面文件可能有语法错误
## 当前调试方案
### 1. 分步测试策略
```javascript
// 第一步:测试基础跳转
wx.navigateTo({
url: 'pages/profile/profile', // 已知存在的页面
success: (res) => {
console.log('基础跳转成功');
// 第二步:测试表单页面跳转
wx.navigateTo({
url: 'pages/business/loan-products/form/form?mode=add',
// ...
});
}
});
```
### 2. 路径格式测试
- 测试1`pages/business/loan-products/form/form` (不带斜杠)
- 测试2`/pages/business/loan-products/form/form` (带斜杠)
- 测试3检查app.json中的注册路径
### 3. 页面注册验证
```json
{
"pages": [
"pages/business/loan-products/form/form", // 确认这个路径存在
// ...
]
}
```
## 已完成的修复
### 1. 事件冲突修复 ✅
- 移除了商品项容器的点击事件冲突
- 优化了按钮事件绑定
### 2. 按钮样式优化 ✅
- 增加了按钮高度和点击区域
- 添加了z-index确保按钮可见
### 3. 调试信息添加 ✅
- 添加了详细的成功/失败回调
- 添加了错误信息显示
### 4. 测试页面创建 ✅
- 创建了简化的测试页面
- 验证了基础跳转功能
## 当前测试状态
### ✅ 成功的部分
- 按钮点击事件正常触发
- 可以跳转到测试页面
- 控制台日志正常输出
### ❌ 失败的部分
- 无法跳转到表单页面
- 错误信息显示页面未找到
## 下一步调试计划
### 1. 立即测试
1. 点击"新增商品"按钮
2. 观察是否先跳转到个人中心页面
3. 观察是否自动返回并尝试跳转到表单页面
4. 查看控制台错误信息
### 2. 路径格式验证
如果基础跳转成功但表单页面跳转失败:
- 检查app.json中的页面注册
- 验证文件路径是否正确
- 检查页面文件是否存在
### 3. 页面文件检查
如果路径正确但仍然失败:
- 检查form.js是否有语法错误
- 检查form.wxml是否有语法错误
- 检查form.json配置是否正确
## 可能的解决方案
### 方案1修复页面路径格式
```javascript
// 当前(可能有问题)
url: 'pages/business/loan-products/form/form?mode=add'
// 修复后
url: '/pages/business/loan-products/form/form?mode=add'
```
### 方案2检查页面注册
确保app.json中的路径与跳转路径完全一致
```json
{
"pages": [
"pages/business/loan-products/form/form"
]
}
```
### 方案3简化页面结构
如果问题持续,考虑:
- 将表单页面移到更简单的路径
- 使用不同的页面命名方式
- 检查是否有特殊字符或路径长度问题
## 调试命令
### 检查文件存在
```bash
ls -la bank_mini_program/pages/business/loan-products/form/
```
### 检查页面注册
```bash
grep -A 10 '"pages"' bank_mini_program/app.json
```
### 检查JS语法
```bash
node -c bank_mini_program/pages/business/loan-products/form/form.js
```
## 预期结果
### 成功情况
- 点击"新增商品"按钮后,先跳转到个人中心
- 1秒后自动返回然后跳转到表单页面
- 表单页面正常加载,显示"商品管理"标题
### 失败情况
- 如果个人中心跳转失败:基础路径格式问题
- 如果表单页面跳转失败:页面注册或文件问题
- 如果都失败:更严重的配置问题
## 联系信息
请测试后提供:
1. 控制台的完整日志
2. 是否看到个人中心页面
3. 是否看到表单页面
4. 任何新的错误信息

View File

@@ -0,0 +1,177 @@
# 银行管理系统小程序 - 商品管理功能完成总结
## 功能概述
已成功为银行管理系统小程序的业务管理页面添加了完整的商品管理功能,包括新增、编辑、删除(下架)商品,并连接了后端接口。
## 完成的功能
### 1. 商品列表页面增强 (`pages/business/loan-products/loan-products.*`)
#### 新增功能
- **新增商品按钮**:在筛选栏右侧添加了"新增商品"按钮
- **商品操作按钮**:为每个商品项添加了"修改信息"和"产品下架"按钮
- **删除确认**:删除商品时显示确认对话框
- **认证检查**:添加了登录状态检查
#### 界面改进
- 优化了筛选栏布局,支持新增按钮
- 为每个商品项添加了操作按钮区域
- 改进了按钮样式和交互效果
### 2. 商品表单页面 (`pages/business/loan-products/form/form.*`)
#### 页面功能
- **新增模式**:支持创建新的贷款商品
- **编辑模式**:支持修改现有商品信息
- **表单验证**:完整的输入验证和错误提示
- **数据加载**:编辑模式下自动加载商品详情
#### 表单字段
- **基本信息**:产品名称、产品代码、产品类型
- **贷款信息**:贷款额度范围、贷款期限、利率
- **服务信息**:服务区域、服务电话、风险等级、所需材料、产品描述
- **状态设置**:在售状态开关
#### 技术特性
- **选择器支持**修复了微信小程序picker组件的兼容性问题
- **计算属性**:实现了动态的选择器索引和标签更新
- **响应式设计**:适配不同屏幕尺寸
- **加载状态**:完整的加载提示和错误处理
### 3. API服务扩展 (`services/apiService.js`)
#### 新增接口方法
- `getDetail(id)` - 获取商品详情
- `create(data)` - 创建商品
- `update(id, data)` - 更新商品
- `delete(id)` - 删除商品
- `toggleStatus(id, status)` - 上架/下架商品
### 4. 页面注册 (`app.json`)
- 注册了新的表单页面路径
- 确保页面路由正常工作
## 技术实现细节
### 1. 微信小程序兼容性修复
**问题**WXML中使用了复杂的JavaScript表达式导致编译错误
```javascript
// 错误的写法
value="{{typeOptions.findIndex(item => item.value === formData.productType)}}"
```
**解决方案**:使用计算属性和数据绑定
```javascript
// 正确的写法
data: {
productTypeIndex: 0,
currentProductTypeLabel: '请选择产品类型'
}
```
### 2. 选择器组件优化
**实现方式**
- 在data中维护选择器的索引和显示标签
- 通过`updatePickerIndexes()`方法同步数据
-`onPickerChange`中更新索引和标签
### 3. 表单验证
**验证规则**
- 产品名称为必填项
- 贷款额度范围必须填写且最小值小于最大值
- 利率为必填项
- 支持实时验证和错误提示
### 4. 数据映射
**字段映射策略**
- 支持多种字段名格式
- 向后兼容现有数据结构
- 空值处理和默认值设置
## 用户界面设计
### 1. 商品列表页面
- **新增按钮**:蓝色主题,位于筛选栏右侧
- **操作按钮**:每个商品项底部显示"修改信息"和"产品下架"按钮
- **按钮样式**:圆角设计,悬停效果,颜色区分功能
### 2. 商品表单页面
- **分段布局**:基本信息、贷款信息、服务信息、状态设置
- **表单控件**:输入框、选择器、开关、文本域
- **操作按钮**:底部固定,取消和提交按钮
- **加载状态**:全屏遮罩,加载提示
## 后端接口对接
### 1. 接口路径
- 商品列表:`GET /loan-products`
- 商品详情:`GET /loan-products/:id`
- 创建商品:`POST /loan-products`
- 更新商品:`PUT /loan-products/:id`
- 删除商品:`DELETE /loan-products/:id`
### 2. 数据格式
- 支持完整的商品信息字段
- 包含验证和错误处理
- 统一的响应格式
## 使用说明
### 1. 新增商品
1. 进入商品管理页面
2. 点击右上角"新增商品"按钮
3. 填写商品信息
4. 点击"新增"按钮提交
### 2. 编辑商品
1. 在商品列表中找到要编辑的商品
2. 点击"修改信息"按钮
3. 修改商品信息
4. 点击"修改"按钮保存
### 3. 下架商品
1. 在商品列表中找到要下架的商品
2. 点击"产品下架"按钮
3. 确认下架操作
## 注意事项
1. **认证要求**:所有操作都需要用户登录
2. **数据验证**:表单提交前会进行完整验证
3. **错误处理**:网络错误和业务错误都有相应提示
4. **兼容性**:修复了微信小程序的兼容性问题
5. **用户体验**:提供加载状态和操作反馈
## 文件结构
```
bank_mini_program/
├── pages/business/loan-products/
│ ├── loan-products.js # 商品列表页面逻辑
│ ├── loan-products.wxml # 商品列表页面结构
│ ├── loan-products.wxss # 商品列表页面样式
│ └── form/
│ ├── form.js # 商品表单页面逻辑
│ ├── form.wxml # 商品表单页面结构
│ ├── form.wxss # 商品表单页面样式
│ └── form.json # 商品表单页面配置
├── services/
│ └── apiService.js # API服务已扩展
└── app.json # 页面注册(已更新)
```
## 更新日志
- **v1.2.0** (2024-01-15)
- 新增商品管理功能
- 添加新增、编辑、删除商品功能
- 修复微信小程序兼容性问题
- 完善表单验证和错误处理
- 优化用户界面和交互体验
- 连接后端API接口

View File

@@ -0,0 +1,117 @@
# 银行小程序个人中心菜单更新
## 更新内容
### 1. 新增功能菜单
在修改密码下方添加了两个新的功能菜单:
- **硬件管理** ⚙️
- **人员管理** 👥
### 2. 样式优化
- 移除了渐变紫色背景 (`linear-gradient(135deg, #667eea 0%, #764ba2 100%)`)
- 改为简洁的蓝色背景 (`#1890ff`)
- 保持整体设计风格的一致性
## 修改的文件
### 1. bank_mini_program/pages/profile/profile.wxml
```xml
<!-- 新增的菜单项 -->
<view class="menu-item" bindtap="hardwareManagement">
<view class="menu-icon">⚙️</view>
<view class="menu-text">硬件管理</view>
<view class="menu-arrow">></view>
</view>
<view class="menu-item" bindtap="personnelManagement">
<view class="menu-icon">👥</view>
<view class="menu-text">人员管理</view>
<view class="menu-arrow">></view>
</view>
```
### 2. bank_mini_program/pages/profile/profile.wxss
```css
/* 修改前 */
.profile-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
/* ... */
}
/* 修改后 */
.profile-header {
background: #1890ff;
/* ... */
}
```
### 3. bank_mini_program/pages/profile/profile.js
```javascript
/**
* 硬件管理
*/
hardwareManagement() {
wx.showToast({
title: '硬件管理功能开发中',
icon: 'none'
})
},
/**
* 人员管理
*/
personnelManagement() {
wx.showToast({
title: '人员管理功能开发中',
icon: 'none'
})
}
```
## 功能说明
### 当前状态
- 两个新功能目前显示"功能开发中"提示
- 为后续功能开发预留了接口
### 菜单顺序
1. 修改密码 🔒
2. 硬件管理 ⚙️ (新增)
3. 人员管理 👥 (新增)
4. 关于我们
### 样式特点
- 使用统一的蓝色主题 (`#1890ff`)
- 保持简洁现代的设计风格
- 移除了复杂的渐变效果
- 图标和文字对齐一致
## 后续开发建议
### 硬件管理功能
- 设备列表查看
- 设备状态监控
- 设备配置管理
- 设备维护记录
### 人员管理功能
- 员工信息管理
- 权限分配
- 角色管理
- 操作日志
## 测试验证
1. **菜单显示**:确认新增的两个菜单项正确显示
2. **点击响应**:点击菜单项显示相应的提示信息
3. **样式效果**:确认头部背景色已更改为蓝色
4. **整体布局**:确认菜单项间距和对齐正确
## 更新日志
- **v1.1.0** (2024-01-15)
- 新增硬件管理菜单项
- 新增人员管理菜单项
- 优化头部背景样式,移除渐变效果
- 保持功能开发中的提示状态

View File

@@ -0,0 +1,193 @@
# 银行管理系统 - 用户ID映射问题修复
## 问题描述
用户反馈:填写了所有必填字段后,接口仍然报错 `{success: false, message: "请填写所有必填字段"}`
## 问题分析
经过深入检查发现问题不在前端字段映射而在后端用户ID映射不一致
### 1. 数据库模型要求
```javascript
// LoanProduct.js 模型定义
createdBy: {
type: DataTypes.INTEGER,
allowNull: false, // 必填字段
field: 'created_by',
comment: '创建人ID'
}
```
### 2. 后端控制器不一致
```javascript
// loanContractController.js (正确)
createdBy: req.user?.id
// loanProductController.js (错误)
createdBy: req.user.userId // ❌ 应该是 req.user.id
```
### 3. 认证中间件设置
认证中间件设置的是 `req.user.id`,但贷款商品控制器使用的是 `req.user.userId`
## 解决方案
修复后端控制器中的用户ID映射统一使用 `req.user.id`
### 1. 用户信息检查
```javascript
// 修复前
if (!req.user || !req.user.userId) {
return res.status(401).json({
success: false,
message: '用户信息无效'
});
}
// 修复后
if (!req.user || !req.user.id) {
return res.status(401).json({
success: false,
message: '用户信息无效'
});
}
```
### 2. 创建记录
```javascript
// 修复前
createdBy: req.user.userId,
updatedBy: req.user.userId
// 修复后
createdBy: req.user.id,
updatedBy: req.user.id
```
### 3. 更新记录
```javascript
// 修复前
product.updatedBy = req.user.userId;
// 修复后
product.updatedBy = req.user.id;
```
### 4. 批量更新
```javascript
// 修复前
updatedBy: req.user.userId
// 修复后
updatedBy: req.user.id
```
## 修复的文件
### bank-backend/controllers/loanProductController.js
- 修复用户信息检查逻辑
- 修复创建记录时的用户ID设置
- 修复更新记录时的用户ID设置
- 修复批量更新时的用户ID设置
## 技术细节
### 1. 认证中间件设置
```javascript
// 认证中间件通常设置
req.user = {
id: userId, // ✅ 正确的字段名
username: username,
// ... 其他用户信息
}
```
### 2. 数据库约束
```javascript
// 数据库模型定义
createdBy: {
type: DataTypes.INTEGER,
allowNull: false, // 必填字段不能为null
field: 'created_by'
}
```
### 3. 错误原因
`req.user.userId``undefined` 时:
- 数据库插入失败
- 返回"请填写所有必填字段"错误
- 实际是 `createdBy` 字段为 `null` 导致的
## 修复后的效果
### 1. 数据完整性
- `createdBy` 字段正确设置
- 数据库约束满足
- 记录创建成功
### 2. 错误消除
- 不再出现"请填写所有必填字段"错误
- 表单提交成功
- 数据持久化正常
### 3. 一致性提升
- 前后端字段映射统一
- 用户ID处理一致
- 代码维护性提升
## 验证步骤
### 1. 前端测试
1. 填写所有必填字段
2. 点击提交按钮
3. 检查控制台日志
4. 验证提交成功
### 2. 后端验证
1. 检查 `createdBy` 字段是否正确设置
2. 验证数据库记录创建成功
3. 确认用户ID映射正确
### 3. 数据库检查
```sql
-- 检查创建的记录
SELECT id, product_name, created_by, created_at
FROM loan_products
ORDER BY created_at DESC
LIMIT 5;
```
## 相关文件
### 修改的文件
- `bank-backend/controllers/loanProductController.js`
- 修复用户ID映射问题
- 统一使用 `req.user.id`
### 参考的文件
- `bank-backend/controllers/loanContractController.js`
- 正确的用户ID使用方式
- 作为修复参考
## 更新日志
- **v1.3.2** (2024-01-15)
- 修复后端用户ID映射问题
- 统一使用 `req.user.id` 替代 `req.user.userId`
- 解决"请填写所有必填字段"错误
- 提升数据完整性
## 注意事项
1. **一致性**确保所有控制器使用相同的用户ID字段
2. **测试**:验证修复后的功能正常
3. **文档**更新API文档中的用户ID说明
4. **监控**关注后续的用户ID相关错误
## 后续优化
1. **统一规范**制定用户ID使用规范
2. **中间件优化**:确保认证中间件设置一致
3. **错误处理**改进用户ID缺失的错误提示
4. **测试覆盖**增加用户ID相关的单元测试

View File

@@ -1,21 +1,99 @@
// app.js
const auth = require('./utils/auth.js')
App({
onLaunch() {
// 展示本地存储能力
const logs = wx.getStorageSync('logs') || []
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)
console.log('银行管理系统小程序启动')
// 检查登录状态
this.checkAuthStatus()
// 设置全局错误处理
this.setupErrorHandler()
},
// 登录
wx.login({
success: res => {
// 发送 res.code 到后台换取 openId, sessionKey, unionId
console.log('登录成功', res.code)
onShow() {
console.log('银行管理系统小程序显示')
},
onHide() {
console.log('银行管理系统小程序隐藏')
},
/**
* 检查认证状态
*/
checkAuthStatus() {
try {
const isAuthenticated = auth.isAuthenticated()
console.log('认证状态:', isAuthenticated)
if (isAuthenticated) {
const userInfo = auth.getUser()
this.globalData.userInfo = userInfo
console.log('用户信息:', userInfo)
}
} catch (error) {
console.error('检查认证状态错误:', error)
}
},
/**
* 设置全局错误处理
*/
setupErrorHandler() {
// 监听未处理的Promise拒绝
wx.onUnhandledRejection((res) => {
console.error('未处理的Promise拒绝:', res)
// 如果是认证相关错误,清除认证信息
if (res.reason && res.reason.message && res.reason.message.includes('认证')) {
auth.clearAuth()
this.globalData.userInfo = null
}
})
},
/**
* 全局登录方法
*/
login(userInfo) {
this.globalData.userInfo = userInfo
console.log('全局登录:', userInfo)
},
/**
* 全局登出方法
*/
logout() {
this.globalData.userInfo = null
auth.clearAuth()
console.log('全局登出')
},
/**
* 检查是否需要登录
*/
requireAuth() {
if (!auth.isAuthenticated()) {
wx.showModal({
title: '需要登录',
content: '此功能需要登录后才能使用',
showCancel: false,
success: () => {
wx.reLaunch({
url: '/pages/login/login'
})
}
})
return false
}
return true
},
globalData: {
userInfo: null,
baseUrl: 'http://localhost:5352/api'
baseUrl: 'https://ad.ningmuyun.com/bank/api',
version: '1.0.0'
}
})

View File

@@ -4,10 +4,13 @@
"pages/projects/projects",
"pages/business/business",
"pages/business/loan-products/loan-products",
"pages/business/loan-products/form/form",
"pages/business/loan-products/test-form",
"pages/business/loan-applications/loan-applications",
"pages/business/loan-contracts/loan-contracts",
"pages/business/loan-releases/loan-releases",
"pages/profile/profile",
"pages/profile/change-password",
"pages/login/login",
"pages/dashboard/dashboard",
"pages/customers/customers",

View File

@@ -1,5 +1,6 @@
// pages/business/business.js
const { apiService } = require('../../services/apiService');
const auth = require('../../utils/auth.js');
Page({
data: {
@@ -12,10 +13,26 @@ Page({
},
onLoad() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadBusinessStats();
},
onShow() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadBusinessStats();
},

View File

@@ -1,5 +1,6 @@
// pages/business/loan-applications/loan-applications.js
const { apiService } = require('../../../services/apiService');
const auth = require('../../../utils/auth.js');
Page({
data: {
@@ -22,10 +23,26 @@ Page({
},
onLoad() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadApplications();
},
onShow() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadApplications();
},
@@ -46,9 +63,28 @@ Page({
if (response.success) {
const applications = response.data.applications.map(application => ({
...application,
// 字段映射 - 根据接口返回数据调整
applicantName: application.borrowerName || application.farmerName || application.applicantName,
applicantId: application.borrowerIdNumber || application.id,
type: application.type,
typeText: this.data.typeMap[application.type] || application.type,
status: application.status,
statusText: this.data.statusMap[application.status] || application.status,
applicationTime: this.formatDate(application.applicationTime)
amount: application.amount || application.loanAmount,
term: application.term || application.loanTerm,
interestRate: application.interestRate,
applicationTime: this.formatDate(application.applicationTime || application.created_at),
phone: application.phone,
purpose: application.purpose,
applicationNumber: application.applicationNumber,
applicationQuantity: application.applicationQuantity,
assetType: application.assetType,
borrowerIdNumber: application.borrowerIdNumber,
borrowerName: application.borrowerName,
farmerName: application.farmerName,
approvedTime: application.approvedTime,
rejectedTime: application.rejectedTime,
remark: application.remark
}));
this.setData({

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -50,6 +50,11 @@
<text class="info-value">{{item.applicantName}}</text>
</view>
<view class="info-row">
<text class="info-label">申请人ID</text>
<text class="info-value">{{item.applicantId}}</text>
</view>
<view class="info-row">
<text class="info-label">申请类型:</text>
<text class="info-value">{{item.typeText}}</text>
@@ -84,6 +89,26 @@
<text class="info-label">申请用途:</text>
<text class="info-value">{{item.purpose}}</text>
</view>
<view class="info-row">
<text class="info-label">资产类型:</text>
<text class="info-value">{{item.assetType || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">申请数量:</text>
<text class="info-value">{{item.applicationQuantity || '1'}}</text>
</view>
<view class="info-row">
<text class="info-label">身份证号:</text>
<text class="info-value">{{item.borrowerIdNumber || '暂无'}}</text>
</view>
<view class="info-row" wx:if="{{item.remark}}">
<text class="info-label">备注:</text>
<text class="info-value">{{item.remark}}</text>
</view>
</view>
</view>
</view>

View File

@@ -1,5 +1,6 @@
// pages/business/loan-contracts/loan-contracts.js
const { apiService } = require('../../../services/apiService');
const auth = require('../../../utils/auth.js');
Page({
data: {
@@ -11,20 +12,39 @@ Page({
'personal': '个人贷款',
'mortgage': '住房贷款',
'business': '企业贷款',
'agricultural': '农业贷款'
'agricultural': '农业贷款',
'livestock_collateral': '养殖抵押贷款'
},
statusMap: {
'active': '生效中',
'expired': '已到期',
'terminated': '已终止'
'terminated': '已终止',
'completed': '已完成',
'pending': '待处理'
}
},
onLoad() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadContracts();
},
onShow() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadContracts();
},
@@ -45,10 +65,31 @@ Page({
if (response.success) {
const contracts = response.data.contracts.map(contract => ({
...contract,
// 字段映射 - 根据接口返回数据调整
customerName: contract.borrowerName || contract.farmerName || contract.customerName,
customerId: contract.borrowerIdNumber || contract.customerId,
type: contract.type,
typeText: this.data.typeMap[contract.type] || contract.type,
status: contract.status,
statusText: this.data.statusMap[contract.status] || contract.status,
signDate: this.formatDate(contract.signDate),
expiryDate: this.formatDate(contract.expiryDate)
amount: contract.amount,
term: contract.term,
interestRate: contract.interestRate,
signDate: this.formatDate(contract.contractTime || contract.signDate),
expiryDate: this.formatDate(contract.maturityTime || contract.expiryDate),
phone: contract.phone,
contractNumber: contract.contractNumber,
applicationNumber: contract.applicationNumber,
applicationQuantity: contract.applicationQuantity,
assetType: contract.assetType,
purpose: contract.purpose,
productName: contract.productName,
paidAmount: contract.paidAmount,
remainingAmount: contract.remainingAmount,
repaymentProgress: contract.repaymentProgress,
completedTime: contract.completedTime,
disbursementTime: contract.disbursementTime,
remark: contract.remark
}));
this.setData({

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -17,7 +17,7 @@
<picker
bindchange="onFilterChange"
value="{{filterStatus}}"
range="{{['全部', '生效中', '已到期', '已终止']}}"
range="{{['全部', '生效中', '已到期', '已终止', '已完成', '待处理']}}"
range-key=""
>
<view class="filter-item">
@@ -47,7 +47,12 @@
<view class="contract-info">
<view class="info-row">
<text class="info-label">客户姓名:</text>
<text class="info-value">{{item.customerName}}</text>
<text class="info-value">{{item.customerName || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">客户ID</text>
<text class="info-value">{{item.customerId || '暂无'}}</text>
</view>
<view class="info-row">
@@ -72,18 +77,58 @@
<view class="info-row">
<text class="info-label">签订时间:</text>
<text class="info-value">{{item.signDate}}</text>
<text class="info-value">{{item.signDate || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">到期时间:</text>
<text class="info-value">{{item.expiryDate}}</text>
<text class="info-value">{{item.expiryDate || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">联系电话:</text>
<text class="info-value">{{item.phone}}</text>
</view>
<view class="info-row">
<text class="info-label">申请单号:</text>
<text class="info-value">{{item.applicationNumber || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">资产类型:</text>
<text class="info-value">{{item.assetType || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">申请用途:</text>
<text class="info-value">{{item.purpose || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">产品名称:</text>
<text class="info-value">{{item.productName || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">已还金额:</text>
<text class="info-value">{{item.paidAmount || 0}}元</text>
</view>
<view class="info-row">
<text class="info-label">剩余金额:</text>
<text class="info-value">{{item.remainingAmount || 0}}元</text>
</view>
<view class="info-row">
<text class="info-label">还款进度:</text>
<text class="info-value">{{item.repaymentProgress || 0}}%</text>
</view>
<view class="info-row" wx:if="{{item.remark}}">
<text class="info-label">备注:</text>
<text class="info-value">{{item.remark}}</text>
</view>
</view>
</view>
</view>

View File

@@ -0,0 +1,316 @@
// pages/business/loan-products/form/form.js
const { apiService } = require('../../../../services/apiService');
const auth = require('../../../../utils/auth.js');
Page({
data: {
mode: 'add', // add 或 edit
productId: null,
loading: false,
formData: {
productName: '',
productCode: '',
productType: 'personal',
minLoanAmount: '',
maxLoanAmount: '',
minLoanTerm: '',
maxLoanTerm: '',
interestRate: '',
productDescription: '',
serviceArea: '',
servicePhone: '',
riskLevel: 'LOW',
requiredDocuments: '',
onSaleStatus: true
},
typeOptions: [
{ value: 'personal', label: '个人贷款' },
{ value: 'mortgage', label: '住房贷款' },
{ value: 'business', label: '企业贷款' },
{ value: 'agricultural', label: '农业贷款' },
{ value: 'livestock_collateral', label: '养殖抵押贷款' }
],
riskLevelOptions: [
{ value: 'LOW', label: '低风险' },
{ value: 'MEDIUM', label: '中风险' },
{ value: 'HIGH', label: '高风险' }
],
// 计算属性
productTypeIndex: 0,
riskLevelIndex: 0,
currentProductTypeLabel: '请选择产品类型',
currentRiskLevelLabel: '请选择风险等级'
},
onLoad(options) {
console.log('表单页面加载', options);
// 检查认证状态
if (!auth.isAuthenticated()) {
console.log('用户未登录,跳转到登录页面');
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
const { mode, productId } = options;
console.log('表单模式:', mode, '商品ID:', productId);
this.setData({
mode: mode || 'add',
productId: productId || null
});
// 初始化计算属性
this.updatePickerIndexes();
if (mode === 'edit' && productId) {
this.loadProductDetail(productId);
}
},
// 更新选择器的索引和标签
updatePickerIndexes() {
const { formData, typeOptions, riskLevelOptions } = this.data;
// 更新产品类型索引
const productTypeIndex = typeOptions.findIndex(item => item.value === formData.productType);
const currentProductTypeLabel = productTypeIndex >= 0 ? typeOptions[productTypeIndex].label : '请选择产品类型';
// 更新风险等级索引
const riskLevelIndex = riskLevelOptions.findIndex(item => item.value === formData.riskLevel);
const currentRiskLevelLabel = riskLevelIndex >= 0 ? riskLevelOptions[riskLevelIndex].label : '请选择风险等级';
this.setData({
productTypeIndex: productTypeIndex >= 0 ? productTypeIndex : 0,
riskLevelIndex: riskLevelIndex >= 0 ? riskLevelIndex : 0,
currentProductTypeLabel,
currentRiskLevelLabel
});
},
// 加载商品详情(编辑模式)
async loadProductDetail(productId) {
try {
this.setData({ loading: true });
const response = await apiService.loanProducts.getDetail(productId);
if (response.success) {
const product = response.data;
this.setData({
formData: {
productName: product.productName || '',
productCode: product.productCode || '',
productType: product.productType || 'personal',
minLoanAmount: product.minLoanAmount || '',
maxLoanAmount: product.maxLoanAmount || '',
minLoanTerm: product.minLoanTerm || '',
maxLoanTerm: product.maxLoanTerm || '',
interestRate: product.interestRate || '',
productDescription: product.productDescription || '',
serviceArea: product.serviceArea || '',
servicePhone: product.servicePhone || '',
riskLevel: product.riskLevel || 'LOW',
requiredDocuments: product.requiredDocuments || '',
onSaleStatus: product.onSaleStatus !== false
}
});
// 更新选择器索引
this.updatePickerIndexes();
} else {
throw new Error(response.message || '获取商品详情失败');
}
} catch (error) {
console.error('加载商品详情失败:', error);
wx.showToast({
title: error.message || '加载失败',
icon: 'error'
});
} finally {
this.setData({ loading: false });
}
},
// 输入处理
onInputChange(e) {
const { field } = e.currentTarget.dataset;
const { value } = e.detail;
this.setData({
[`formData.${field}`]: value
});
},
// 选择器处理
onPickerChange(e) {
const { field } = e.currentTarget.dataset;
const { value } = e.detail;
let selectedValue = '';
let selectedLabel = '';
if (field === 'productType') {
selectedValue = this.data.typeOptions[value].value;
selectedLabel = this.data.typeOptions[value].label;
this.setData({
[`formData.${field}`]: selectedValue,
productTypeIndex: value,
currentProductTypeLabel: selectedLabel
});
} else if (field === 'riskLevel') {
selectedValue = this.data.riskLevelOptions[value].value;
selectedLabel = this.data.riskLevelOptions[value].label;
this.setData({
[`formData.${field}`]: selectedValue,
riskLevelIndex: value,
currentRiskLevelLabel: selectedLabel
});
}
},
// 开关处理
onSwitchChange(e) {
const { field } = e.currentTarget.dataset;
const { value } = e.detail;
this.setData({
[`formData.${field}`]: value
});
},
// 表单验证
validateForm() {
const { formData } = this.data;
if (!formData.productName.trim()) {
wx.showToast({
title: '请输入产品名称',
icon: 'none'
});
return false;
}
if (!formData.productType) {
wx.showToast({
title: '请选择产品类型',
icon: 'none'
});
return false;
}
if (!formData.minLoanAmount || !formData.maxLoanAmount) {
wx.showToast({
title: '请输入贷款额度范围',
icon: 'none'
});
return false;
}
if (parseFloat(formData.minLoanAmount) >= parseFloat(formData.maxLoanAmount)) {
wx.showToast({
title: '最小额度应小于最大额度',
icon: 'none'
});
return false;
}
if (!formData.interestRate) {
wx.showToast({
title: '请输入利率',
icon: 'none'
});
return false;
}
if (!formData.riskLevel) {
wx.showToast({
title: '请选择风险等级',
icon: 'none'
});
return false;
}
return true;
},
// 提交表单
async submitForm() {
if (!this.validateForm()) {
return;
}
try {
this.setData({ loading: true });
const { mode, productId, formData } = this.data;
// 转换字段名为后端期望的格式
const submitData = {
productName: formData.productName,
productCode: formData.productCode,
productType: formData.productType,
// 后端期望的字段名
loanAmount: formData.maxLoanAmount || formData.minLoanAmount, // 使用最大贷款额度作为贷款金额
loanTerm: formData.maxLoanTerm || formData.minLoanTerm, // 使用最大贷款期限作为贷款期限
minLoanAmount: formData.minLoanAmount,
maxLoanAmount: formData.maxLoanAmount,
minLoanTerm: formData.minLoanTerm,
maxLoanTerm: formData.maxLoanTerm,
interestRate: formData.interestRate,
productDescription: formData.productDescription,
serviceArea: formData.serviceArea,
servicePhone: formData.servicePhone,
riskLevel: formData.riskLevel,
requiredDocuments: formData.requiredDocuments,
onSaleStatus: formData.onSaleStatus
};
// 调试信息:打印发送的数据
console.log('发送的数据:', submitData);
console.log('必填字段检查:');
console.log('productName:', submitData.productName, '是否为空:', !submitData.productName);
console.log('loanAmount:', submitData.loanAmount, '是否为空:', !submitData.loanAmount);
console.log('loanTerm:', submitData.loanTerm, '是否为空:', !submitData.loanTerm);
console.log('interestRate:', submitData.interestRate, '是否为空:', !submitData.interestRate);
console.log('serviceArea:', submitData.serviceArea, '是否为空:', !submitData.serviceArea);
console.log('servicePhone:', submitData.servicePhone, '是否为空:', !submitData.servicePhone);
let response;
if (mode === 'add') {
response = await apiService.loanProducts.create(submitData);
} else {
response = await apiService.loanProducts.update(productId, submitData);
}
if (response.success) {
wx.showToast({
title: mode === 'add' ? '新增成功' : '修改成功',
icon: 'success'
});
setTimeout(() => {
wx.navigateBack();
}, 1500);
} else {
throw new Error(response.message || '操作失败');
}
} catch (error) {
console.error('提交失败:', error);
wx.showToast({
title: error.message || '操作失败',
icon: 'error'
});
} finally {
this.setData({ loading: false });
}
},
// 取消
cancel() {
wx.navigateBack();
}
});

View File

@@ -0,0 +1,6 @@
{
"navigationBarTitleText": "商品管理",
"navigationBarBackgroundColor": "#1890ff",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,228 @@
<!--pages/business/loan-products/form/form.wxml-->
<view class="form-container">
<view class="form-header">
<text class="form-title">{{mode === 'add' ? '新增商品' : '修改信息'}}</text>
</view>
<view class="form-content">
<!-- 基本信息 -->
<view class="form-section">
<view class="section-title">基本信息</view>
<view class="form-item">
<text class="form-label">
产品名称
<text class="required-asterisk">*</text>
</text>
<input
class="form-input"
placeholder="请输入产品名称"
value="{{formData.productName}}"
data-field="productName"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">产品代码</text>
<input
class="form-input"
placeholder="请输入产品代码"
value="{{formData.productCode}}"
data-field="productCode"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">
产品类型
<text class="required-asterisk">*</text>
</text>
<picker
class="form-picker"
data-field="productType"
bindchange="onPickerChange"
value="{{productTypeIndex}}"
range="{{typeOptions}}"
range-key="label"
>
<view class="picker-text">
{{currentProductTypeLabel}}
</view>
</picker>
</view>
</view>
<!-- 贷款信息 -->
<view class="form-section">
<view class="section-title">贷款信息</view>
<view class="form-item">
<text class="form-label">
最小贷款额度
<text class="required-asterisk">*</text>
</text>
<input
class="form-input"
placeholder="请输入最小贷款额度"
type="number"
value="{{formData.minLoanAmount}}"
data-field="minLoanAmount"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">
最大贷款额度
<text class="required-asterisk">*</text>
</text>
<input
class="form-input"
placeholder="请输入最大贷款额度"
type="number"
value="{{formData.maxLoanAmount}}"
data-field="maxLoanAmount"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">最小贷款期限</text>
<input
class="form-input"
placeholder="请输入最小贷款期限(月)"
type="number"
value="{{formData.minLoanTerm}}"
data-field="minLoanTerm"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">最大贷款期限</text>
<input
class="form-input"
placeholder="请输入最大贷款期限(月)"
type="number"
value="{{formData.maxLoanTerm}}"
data-field="maxLoanTerm"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">
利率
<text class="required-asterisk">*</text>
</text>
<input
class="form-input"
placeholder="请输入利率(%"
type="digit"
value="{{formData.interestRate}}"
data-field="interestRate"
bindinput="onInputChange"
/>
</view>
</view>
<!-- 服务信息 -->
<view class="form-section">
<view class="section-title">服务信息</view>
<view class="form-item">
<text class="form-label">服务区域</text>
<input
class="form-input"
placeholder="请输入服务区域"
value="{{formData.serviceArea}}"
data-field="serviceArea"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">服务电话</text>
<input
class="form-input"
placeholder="请输入服务电话"
value="{{formData.servicePhone}}"
data-field="servicePhone"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">
风险等级
<text class="required-asterisk">*</text>
</text>
<picker
class="form-picker"
data-field="riskLevel"
bindchange="onPickerChange"
value="{{riskLevelIndex}}"
range="{{riskLevelOptions}}"
range-key="label"
>
<view class="picker-text">
{{currentRiskLevelLabel}}
</view>
</picker>
</view>
<view class="form-item">
<text class="form-label">所需材料</text>
<textarea
class="form-textarea"
placeholder="请输入所需材料"
value="{{formData.requiredDocuments}}"
data-field="requiredDocuments"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">产品描述</text>
<textarea
class="form-textarea"
placeholder="请输入产品描述"
value="{{formData.productDescription}}"
data-field="productDescription"
bindinput="onInputChange"
/>
</view>
</view>
<!-- 状态设置 -->
<view class="form-section">
<view class="section-title">状态设置</view>
<view class="form-item switch-item">
<text class="form-label">在售状态</text>
<switch
checked="{{formData.onSaleStatus}}"
data-field="onSaleStatus"
bindchange="onSwitchChange"
/>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="form-actions">
<button class="action-btn cancel-btn" bindtap="cancel">取消</button>
<button class="action-btn submit-btn" bindtap="submitForm" disabled="{{loading}}">
{{loading ? '提交中...' : (mode === 'add' ? '新增' : '修改')}}
</button>
</view>
<!-- 加载提示 -->
<view class="loading-overlay" wx:if="{{loading}}">
<view class="loading-content">
<text class="loading-text">加载中...</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,193 @@
/* pages/business/loan-products/form/form.wxss */
.form-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.form-header {
background-color: #fff;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
}
.form-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.form-content {
padding: 20rpx;
}
.form-section {
background-color: #fff;
margin-bottom: 20rpx;
border-radius: 8rpx;
padding: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 30rpx;
padding-bottom: 10rpx;
border-bottom: 2rpx solid #1890ff;
}
.form-item {
margin-bottom: 30rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
font-weight: 500;
}
/* 必填字段的*号样式 */
.required-asterisk {
color: #ff4d4f;
font-weight: bold;
margin-left: 4rpx;
}
.form-input {
width: 100%;
height: 80rpx;
background-color: #f8f8f8;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.form-input:focus {
border-color: #1890ff;
background-color: #fff;
}
.form-picker {
width: 100%;
height: 80rpx;
background-color: #f8f8f8;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
box-sizing: border-box;
}
.picker-text {
font-size: 28rpx;
color: #333;
flex: 1;
}
.form-textarea {
width: 100%;
min-height: 120rpx;
background-color: #f8f8f8;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
color: #333;
box-sizing: border-box;
}
.form-textarea:focus {
border-color: #1890ff;
background-color: #fff;
}
.switch-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.form-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
border-top: 1rpx solid #eee;
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
}
.cancel-btn:active {
background-color: #e8e8e8;
}
.submit-btn {
background-color: #1890ff;
color: #fff;
}
.submit-btn:active {
background-color: #40a9ff;
}
.submit-btn[disabled] {
background-color: #ccc;
color: #999;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
background-color: #fff;
padding: 40rpx 60rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 28rpx;
color: #333;
}

View File

@@ -1,5 +1,6 @@
// pages/business/loan-products/loan-products.js
const { apiService } = require('../../../services/apiService');
const auth = require('../../../utils/auth.js');
Page({
data: {
@@ -16,10 +17,26 @@ Page({
},
onLoad() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadProducts();
},
onShow() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadProducts();
},
@@ -40,8 +57,27 @@ Page({
if (response.success) {
const products = response.data.products.map(product => ({
...product,
statusText: product.status === 'active' ? '在售' : '停售',
typeText: this.data.typeMap[product.type] || product.type
// 字段映射 - 根据接口返回数据调整
name: product.productName || product.name,
code: product.productCode || product.code,
type: product.productType || product.type,
status: product.onSaleStatus ? 'active' : 'inactive',
statusText: product.onSaleStatus ? '在售' : '停售',
typeText: this.data.typeMap[product.productType || product.type] || (product.productType || product.type),
minAmount: product.minLoanAmount || product.minAmount,
maxAmount: product.maxLoanAmount || product.maxAmount,
minTerm: product.minLoanTerm || product.minTerm,
maxTerm: product.maxLoanTerm || product.maxTerm,
interestRate: product.interestRate || product.minInterestRate,
maxInterestRate: product.maxInterestRate || product.interestRate,
description: product.productDescription || product.description,
// 新增字段
serviceArea: product.serviceArea,
servicePhone: product.servicePhone,
riskLevel: product.riskLevel,
requiredDocuments: product.requiredDocuments,
supervisionCustomers: product.supervisionCustomers,
totalCustomers: product.totalCustomers
}));
this.setData({
@@ -127,6 +163,123 @@ Page({
});
},
// 新增商品
addProduct() {
console.log('点击新增商品按钮');
// 使用绝对路径跳转到表单页面
wx.navigateTo({
url: '/pages/business/loan-products/form/form?mode=add',
success: (res) => {
console.log('跳转到表单页面成功', res);
},
fail: (err) => {
console.error('跳转到表单页面失败', err);
console.error('错误详情:', JSON.stringify(err));
// 如果失败,尝试跳转到测试页面
wx.navigateTo({
url: '/pages/business/loan-products/test-form?mode=add',
success: (res) => {
console.log('跳转到测试页面成功', res);
},
fail: (err2) => {
console.error('跳转到测试页面也失败', err2);
wx.showModal({
title: '跳转失败',
content: `表单页面错误: ${err.errMsg}\n测试页面错误: ${err2.errMsg}`,
showCancel: false
});
}
});
}
});
},
// 编辑商品
editProduct(e) {
console.log('点击编辑商品按钮', e);
const productId = e.currentTarget.dataset.productId;
console.log('商品ID:', productId);
// 使用绝对路径跳转到表单页面
wx.navigateTo({
url: `/pages/business/loan-products/form/form?mode=edit&productId=${productId}`,
success: (res) => {
console.log('跳转到编辑页面成功', res);
},
fail: (err) => {
console.error('跳转到编辑页面失败', err);
console.error('错误详情:', JSON.stringify(err));
// 如果失败,尝试跳转到测试页面
wx.navigateTo({
url: `/pages/business/loan-products/test-form?mode=edit&productId=${productId}`,
success: (res) => {
console.log('跳转到测试页面成功', res);
},
fail: (err2) => {
console.error('跳转到测试页面也失败', err2);
wx.showModal({
title: '跳转失败',
content: `表单页面错误: ${err.errMsg}\n测试页面错误: ${err2.errMsg}`,
showCancel: false
});
}
});
}
});
},
// 删除商品(下架)
deleteProduct(e) {
console.log('点击删除商品按钮', e);
const productId = e.currentTarget.dataset.productId;
console.log('商品ID:', productId);
const product = this.data.products.find(p => p.id == productId);
wx.showModal({
title: '确认下架',
content: `确定要下架商品"${product?.name || '该商品'}"吗?`,
success: (res) => {
if (res.confirm) {
this.handleDeleteProduct(productId);
}
}
});
},
// 处理删除商品
async handleDeleteProduct(productId) {
try {
wx.showLoading({
title: '下架中...'
});
const response = await apiService.loanProducts.delete(productId);
if (response.success) {
wx.hideLoading();
wx.showToast({
title: '下架成功',
icon: 'success'
});
// 重新加载商品列表
this.loadProducts();
} else {
throw new Error(response.message || '下架失败');
}
} catch (error) {
console.error('下架商品失败:', error);
wx.hideLoading();
wx.showToast({
title: error.message || '下架失败',
icon: 'error'
});
}
},
// 下拉刷新
onPullDownRefresh() {
this.loadProducts().finally(() => {

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -12,7 +12,7 @@
</view>
</view>
<!-- 筛选栏 -->
<!-- 筛选栏和操作按钮 -->
<view class="filter-bar">
<picker
bindchange="onFilterChange"
@@ -26,6 +26,14 @@
<text class="arrow">></text>
</view>
</picker>
<!-- 新增商品按钮 -->
<view class="action-buttons">
<button class="add-btn" bindtap="addProduct">
<text class="btn-icon">+</text>
<text class="btn-text">新增商品</text>
</button>
</view>
</view>
<!-- 商品列表 -->
@@ -35,19 +43,18 @@
wx:for="{{products}}"
wx:key="id"
data-product-id="{{item.id}}"
bindtap="viewProductDetail"
>
<view class="product-header">
<view class="product-header" bindtap="viewProductDetail" data-product-id="{{item.id}}">
<text class="product-name">{{item.name}}</text>
<view class="product-status {{item.status}}">
<text>{{item.statusText}}</text>
</view>
</view>
<view class="product-info">
<view class="product-info" bindtap="viewProductDetail" data-product-id="{{item.id}}">
<view class="info-row">
<text class="info-label">产品代码:</text>
<text class="info-value">{{item.code}}</text>
<text class="info-value">{{item.code || '暂无'}}</text>
</view>
<view class="info-row">
@@ -62,18 +69,50 @@
<view class="info-row">
<text class="info-label">贷款期限:</text>
<text class="info-value">{{item.minTerm}} - {{item.maxTerm}}个月</text>
<text class="info-value">{{item.loanTerm || item.minTerm}}个月</text>
</view>
<view class="info-row">
<text class="info-label">利率范围</text>
<text class="info-value">{{item.interestRate}}% - {{item.maxInterestRate}}%</text>
<text class="info-label">利率:</text>
<text class="info-value">{{item.interestRate}}%</text>
</view>
<view class="info-row">
<text class="info-label">服务区域:</text>
<text class="info-value">{{item.serviceArea || '全国'}}</text>
</view>
<view class="info-row">
<text class="info-label">服务电话:</text>
<text class="info-value">{{item.servicePhone || '暂无'}}</text>
</view>
<view class="info-row">
<text class="info-label">风险等级:</text>
<text class="info-value">{{item.riskLevel || 'LOW'}}</text>
</view>
<view class="info-row">
<text class="info-label">监管客户:</text>
<text class="info-value">{{item.supervisionCustomers}}/{{item.totalCustomers}}</text>
</view>
</view>
<view class="product-desc">
<view class="product-desc" bindtap="viewProductDetail" data-product-id="{{item.id}}">
<text class="desc-text">{{item.description}}</text>
</view>
<!-- 操作按钮 -->
<view class="product-actions">
<button class="action-btn edit-btn" bindtap="editProduct" data-product-id="{{item.id}}">
<text class="btn-icon">✏️</text>
<text class="btn-text">修改信息</text>
</button>
<button class="action-btn delete-btn" bindtap="deleteProduct" data-product-id="{{item.id}}">
<text class="btn-icon">🗑️</text>
<text class="btn-text">产品下架</text>
</button>
</view>
</view>
</view>

View File

@@ -37,15 +37,48 @@
.filter-bar {
background-color: #fff;
border-bottom: 1rpx solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
}
.filter-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
font-size: 28rpx;
color: #333;
flex: 1;
}
.action-buttons {
margin-left: 20rpx;
}
.add-btn {
background-color: #1890ff;
color: #fff;
border: none;
border-radius: 25rpx;
padding: 12rpx 24rpx;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
min-width: 120rpx;
}
.add-btn:active {
background-color: #40a9ff;
}
.btn-icon {
margin-right: 8rpx;
font-size: 20rpx;
}
.btn-text {
font-size: 24rpx;
}
.filter-value {
@@ -131,6 +164,50 @@
line-height: 1.4;
}
/* 商品操作按钮 */
.product-actions {
display: flex;
gap: 20rpx;
margin-top: 20rpx;
padding: 20rpx 0;
border-top: 1rpx solid #f0f0f0;
position: relative;
z-index: 5;
}
.action-btn {
flex: 1;
height: 80rpx;
border-radius: 8rpx;
font-size: 26rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
position: relative;
z-index: 10;
margin: 0 10rpx;
min-width: 120rpx;
}
.edit-btn {
background-color: #1890ff;
color: #fff;
}
.edit-btn:active {
background-color: #40a9ff;
}
.delete-btn {
background-color: #ff4d4f;
color: #fff;
}
.delete-btn:active {
background-color: #ff7875;
}
/* 加载状态 */
.loading {
text-align: center;

View File

@@ -0,0 +1,19 @@
// 测试表单页面
Page({
data: {
mode: '',
productId: ''
},
onLoad(options) {
console.log('测试表单页面加载', options);
this.setData({
mode: options.mode || '未知',
productId: options.productId || '无'
});
},
goBack() {
wx.navigateBack();
}
});

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -0,0 +1,16 @@
<!--测试表单页面-->
<view class="test-container">
<view class="test-header">
<text class="test-title">测试表单页面</text>
</view>
<view class="test-content">
<text class="test-text">这是一个测试页面</text>
<text class="test-text">模式: {{mode}}</text>
<text class="test-text">商品ID: {{productId}}</text>
</view>
<view class="test-actions">
<button class="test-btn" bindtap="goBack">返回</button>
</view>
</view>

View File

@@ -0,0 +1,46 @@
/* 测试表单页面样式 */
.test-container {
padding: 40rpx;
min-height: 100vh;
background: #f5f5f5;
}
.test-header {
background: #1890ff;
color: #fff;
padding: 40rpx;
border-radius: 16rpx;
margin-bottom: 40rpx;
}
.test-title {
font-size: 32rpx;
font-weight: 600;
}
.test-content {
background: #fff;
padding: 40rpx;
border-radius: 16rpx;
margin-bottom: 40rpx;
}
.test-text {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
}
.test-actions {
text-align: center;
}
.test-btn {
background: #1890ff;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
}

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -65,6 +65,26 @@ Page({
},
onLoad() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.initData()
},
onShow() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.initData()
},

View File

@@ -7,11 +7,25 @@ Page({
username: '',
password: '',
showPassword: false,
loading: false
loading: false,
rememberMe: false
},
onLoad() {
// 检查是否已登录
if (auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/index/index'
})
return
}
// 尝试从本地存储恢复用户名
this.loadStoredUsername()
},
onShow() {
// 每次显示页面时检查登录状态
if (auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/index/index'
@@ -19,67 +33,185 @@ Page({
}
},
/**
* 加载存储的用户名
*/
loadStoredUsername() {
try {
const storedUsername = wx.getStorageSync('remembered_username')
if (storedUsername) {
this.setData({
username: storedUsername,
rememberMe: true
})
}
} catch (error) {
console.error('加载存储用户名错误:', error)
}
},
/**
* 用户名输入
*/
onUsernameInput(e) {
this.setData({
username: e.detail.value
username: e.detail.value.trim()
})
},
/**
* 密码输入
*/
onPasswordInput(e) {
this.setData({
password: e.detail.value
})
},
/**
* 切换密码显示
*/
togglePassword() {
this.setData({
showPassword: !this.data.showPassword
})
},
/**
* 切换记住我
*/
onRememberMeChange(e) {
this.setData({
rememberMe: e.detail.value
})
},
/**
* 处理登录
*/
async handleLogin() {
const { username, password, loading } = this.data
const { username, password, loading, rememberMe } = this.data
if (!username.trim() || !password.trim() || loading) return
// 参数验证
if (!username || !password || loading) {
if (!username) {
wx.showToast({
title: '请输入用户名',
icon: 'none'
})
} else if (!password) {
wx.showToast({
title: '请输入密码',
icon: 'none'
})
}
return
}
this.setData({ loading: true })
try {
console.log('开始登录:', username)
// 调用登录服务
const response = await authService.login(username, password)
if (response && response.token) {
if (response && response.success) {
// 保存token
auth.setToken(response.token)
if (response.token) {
auth.setToken(response.token)
}
// 获取用户信息
const userInfo = await authService.getUserInfo()
if (userInfo) {
auth.setUser(userInfo)
// 保存用户信息
if (response.user) {
auth.setUser(response.user)
// 设置全局用户信息
const app = getApp()
if (app && app.login) {
app.login(response.user)
}
} else {
// 如果没有用户信息,尝试获取
try {
const userInfo = await authService.getUserInfo()
if (userInfo) {
auth.setUser(userInfo)
// 设置全局用户信息
const app = getApp()
if (app && app.login) {
app.login(userInfo)
}
}
} catch (error) {
console.warn('获取用户信息失败:', error)
}
}
// 记住用户名
if (rememberMe) {
wx.setStorageSync('remembered_username', username)
} else {
wx.removeStorageSync('remembered_username')
}
// 显示成功提示
wx.showToast({
title: '登录成功',
title: response.message || '登录成功',
icon: 'success',
duration: 2000
})
// 跳转到首页
// 跳转到日检预警页面
setTimeout(() => {
wx.reLaunch({
url: '/pages/index/index'
url: '/pages/warning/warning'
})
}, 1000)
} else {
throw new Error(response.message || '登录失败')
}
} catch (error) {
console.error('登录失败:', error)
// 显示错误提示
wx.showToast({
title: error.message || '登录失败',
title: error.message || '登录失败,请检查用户名和密码',
icon: 'error',
duration: 3000
})
// 如果是认证错误,清除本地存储
if (error.message && error.message.includes('认证')) {
auth.clearAuth()
}
} finally {
this.setData({ loading: false })
}
},
/**
* 快速登录(测试用)
*/
quickLogin() {
this.setData({
username: 'admin',
password: '123456',
rememberMe: true
})
},
/**
* 重置表单
*/
resetForm() {
this.setData({
username: '',
password: '',
showPassword: false,
rememberMe: false
})
}
})

View File

@@ -32,6 +32,17 @@
</view>
</view>
<view class="form-item remember-item">
<label class="remember-label">
<checkbox
checked="{{rememberMe}}"
bindchange="onRememberMeChange"
class="remember-checkbox"
/>
<text class="remember-text">记住我</text>
</label>
</view>
<view class="form-item">
<button
class="login-btn {{(!username || !password || loading) ? 'disabled' : ''}}"
@@ -44,6 +55,9 @@
<view class="login-tips">
<text class="tips-text">默认账号admin / 123456</text>
<view class="quick-login" bindtap="quickLogin">
<text class="quick-login-text">快速登录</text>
</view>
</view>
</view>

View File

@@ -1,7 +1,7 @@
/* pages/login/login.wxss */
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
@@ -22,19 +22,20 @@
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: #fff;
background: #f0f0f0;
border: 2rpx solid #e0e0e0;
}
.title {
font-size: 48rpx;
font-weight: 600;
color: #fff;
color: #333;
margin-bottom: 16rpx;
}
.subtitle {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
color: #666;
}
.login-form {
@@ -50,8 +51,8 @@
.input {
width: 100%;
height: 88rpx;
background: rgba(255, 255, 255, 0.9);
border: none;
background: #f8f8f8;
border: 2rpx solid #e0e0e0;
border-radius: 44rpx;
padding: 0 40rpx;
font-size: 32rpx;
@@ -94,14 +95,51 @@
color: #999;
}
.remember-item {
margin-bottom: 30rpx;
}
.remember-label {
display: flex;
align-items: center;
justify-content: flex-start;
}
.remember-checkbox {
margin-right: 16rpx;
transform: scale(0.8);
}
.remember-text {
font-size: 28rpx;
color: #666;
}
.login-tips {
text-align: center;
margin-top: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.tips-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
color: #999;
margin-bottom: 20rpx;
}
.quick-login {
padding: 16rpx 32rpx;
background: #f0f0f0;
border-radius: 24rpx;
border: 2rpx solid #e0e0e0;
}
.quick-login-text {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
.login-footer {
@@ -112,5 +150,5 @@
.footer-text {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
color: #999;
}

View File

@@ -0,0 +1,203 @@
// pages/profile/change-password.js
const auth = require('../../utils/auth.js')
const authService = require('../../services/authService.js')
Page({
data: {
userInfo: null,
loading: false,
formData: {
oldPassword: '',
newPassword: '',
confirmPassword: ''
}
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadUserInfo()
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 检查登录状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
})
return
}
this.loadUserInfo()
},
/**
* 加载用户信息
*/
loadUserInfo() {
try {
const userInfo = auth.getUser()
if (userInfo) {
this.setData({
userInfo: userInfo
})
}
} catch (error) {
console.error('加载用户信息错误:', error)
}
},
/**
* 输入框内容变化
*/
onInputChange(e) {
const field = e.currentTarget.dataset.field
const value = e.detail.value
this.setData({
[`formData.${field}`]: value
})
},
/**
* 表单验证
*/
validateForm() {
const { oldPassword, newPassword, confirmPassword } = this.data.formData
if (!oldPassword.trim()) {
wx.showToast({
title: '请输入旧密码',
icon: 'none'
})
return false
}
if (!newPassword.trim()) {
wx.showToast({
title: '请输入新密码',
icon: 'none'
})
return false
}
if (newPassword.length < 6) {
wx.showToast({
title: '新密码至少6位',
icon: 'none'
})
return false
}
if (newPassword === oldPassword) {
wx.showToast({
title: '新密码不能与旧密码相同',
icon: 'none'
})
return false
}
if (!confirmPassword.trim()) {
wx.showToast({
title: '请输入确认密码',
icon: 'none'
})
return false
}
if (newPassword !== confirmPassword) {
wx.showToast({
title: '两次输入的新密码不一致',
icon: 'none'
})
return false
}
return true
},
/**
* 确认修改密码
*/
async handleConfirm() {
if (!this.validateForm()) {
return
}
if (this.data.loading) {
return
}
this.setData({ loading: true })
try {
const { oldPassword, newPassword } = this.data.formData
// 调用修改密码接口
const result = await authService.changePassword({
oldPassword,
newPassword
})
if (result.success) {
wx.showToast({
title: '密码修改成功',
icon: 'success',
duration: 2000
})
// 延迟返回上一页
setTimeout(() => {
wx.navigateBack()
}, 1500)
} else {
throw new Error(result.message || '修改密码失败')
}
} catch (error) {
console.error('修改密码错误:', error)
wx.showToast({
title: error.message || '修改密码失败',
icon: 'error'
})
} finally {
this.setData({ loading: false })
}
},
/**
* 重置表单
*/
handleReset() {
if (this.data.loading) {
return
}
this.setData({
formData: {
oldPassword: '',
newPassword: '',
confirmPassword: ''
}
})
wx.showToast({
title: '已重置',
icon: 'success',
duration: 1000
})
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: '银行管理系统',
path: '/pages/index/index'
}
}
})

View File

@@ -0,0 +1,7 @@
{
"usingComponents": {},
"navigationBarTitleText": "修改密码",
"navigationBarBackgroundColor": "#1890ff",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f5f5"
}

View File

@@ -0,0 +1,68 @@
<!--pages/profile/change-password.wxml-->
<view class="change-password-container">
<!-- 页面标题 -->
<view class="page-header">
<view class="header-title">修改密码</view>
</view>
<!-- 表单内容 -->
<view class="form-container">
<view class="form-item">
<text class="form-label">用户名</text>
<view class="form-value">{{userInfo.username || '刘超'}}</view>
</view>
<view class="form-item">
<text class="form-label">旧密码</text>
<input
class="form-input"
type="password"
placeholder="请输入旧密码"
value="{{formData.oldPassword}}"
data-field="oldPassword"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">新密码</text>
<input
class="form-input"
type="password"
placeholder="请输入新密码"
value="{{formData.newPassword}}"
data-field="newPassword"
bindinput="onInputChange"
/>
</view>
<view class="form-item">
<text class="form-label">确认新密码</text>
<input
class="form-input"
type="password"
placeholder="请输入确认密码"
value="{{formData.confirmPassword}}"
data-field="confirmPassword"
bindinput="onInputChange"
/>
</view>
</view>
<!-- 操作按钮 -->
<view class="button-container">
<button class="btn confirm-btn" bindtap="handleConfirm" disabled="{{loading}}">
{{loading ? '修改中...' : '确认'}}
</button>
<button class="btn reset-btn" bindtap="handleReset" disabled="{{loading}}">
重置
</button>
</view>
</view>
<!-- 加载提示 -->
<view class="loading-overlay" wx:if="{{loading}}">
<view class="loading-content">
<text class="loading-text">修改中...</text>
</view>
</view>

View File

@@ -0,0 +1,141 @@
/* pages/profile/change-password.wxss */
.change-password-container {
min-height: 100vh;
background: #f5f5f5;
padding: 0 0 40rpx 0;
}
/* 页面标题 */
.page-header {
background: #1890ff;
padding: 40rpx 40rpx 30rpx 40rpx;
text-align: center;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #fff;
}
/* 表单容器 */
.form-container {
background: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 40rpx;
}
.form-item {
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 500;
}
.form-value {
font-size: 32rpx;
color: #666;
padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.form-input {
font-size: 32rpx;
color: #333;
padding: 24rpx 0;
border-bottom: 1rpx solid #e8e8e8;
background: transparent;
}
.form-input:focus {
border-bottom-color: #1890ff;
}
/* 按钮容器 */
.button-container {
padding: 40rpx;
display: flex;
gap: 24rpx;
}
.btn {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
border: none;
position: relative;
}
.confirm-btn {
background: #1890ff;
color: #fff;
}
.confirm-btn:active {
background: #40a9ff;
}
.confirm-btn[disabled] {
background: #d9d9d9;
color: #999;
}
.reset-btn {
background: #fff;
color: #666;
border: 2rpx solid #d9d9d9;
}
.reset-btn:active {
background: #f5f5f5;
}
.reset-btn[disabled] {
background: #f5f5f5;
color: #999;
border-color: #e8e8e8;
}
/* 加载提示 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
background: #fff;
padding: 40rpx 60rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 28rpx;
color: #333;
}

View File

@@ -0,0 +1,66 @@
// pages/profile/change-password/change-password.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View File

@@ -0,0 +1,2 @@
<!--pages/profile/change-password/change-password.wxml-->
<text>pages/profile/change-password/change-password.wxml</text>

View File

@@ -0,0 +1 @@
/* pages/profile/change-password/change-password.wxss */

View File

@@ -1,66 +1,225 @@
// pages/profile/profile.js
const auth = require('../../utils/auth.js')
const authService = require('../../services/authService.js')
Page({
/**
* 页面的初始数据
*/
data: {
userInfo: null,
loading: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
this.loadUserInfo()
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 检查登录状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
})
return
}
this.loadUserInfo()
},
/**
* 生命周期函数--监听页面隐藏
* 加载用户信息
*/
onHide() {
loadUserInfo() {
try {
const userInfo = auth.getUser()
if (userInfo) {
this.setData({
userInfo: userInfo
})
} else {
// 如果没有用户信息,尝试获取
this.refreshUserInfo()
}
} catch (error) {
console.error('加载用户信息错误:', error)
}
},
/**
* 生命周期函数--监听页面卸载
* 刷新用户信息
*/
onUnload() {
async refreshUserInfo() {
if (this.data.loading) return
this.setData({ loading: true })
try {
const userInfo = await authService.refreshUserInfo()
this.setData({
userInfo: userInfo
})
} catch (error) {
console.error('刷新用户信息错误:', error)
wx.showToast({
title: '获取用户信息失败',
icon: 'error'
})
} finally {
this.setData({ loading: false })
}
},
/**
* 编辑个人信息
*/
editProfile() {
wx.showToast({
title: '功能开发中',
icon: 'none'
})
},
/**
* 修改密码
*/
changePassword() {
wx.navigateTo({
url: '/pages/profile/change-password'
})
},
/**
* 硬件管理
*/
hardwareManagement() {
wx.showToast({
title: '硬件管理功能开发中',
icon: 'none'
})
},
/**
* 人员管理
*/
personnelManagement() {
wx.showToast({
title: '人员管理功能开发中',
icon: 'none'
})
},
/**
* 关于我们
*/
aboutUs() {
wx.showModal({
title: '关于银行管理系统',
content: '版本1.0.0\n\n银行管理系统小程序提供项目管理和业务处理功能。',
showCancel: false,
confirmText: '确定'
})
},
/**
* 退出登录
*/
logout() {
wx.showModal({
title: '确认退出',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
this.handleLogout()
}
}
})
},
/**
* 处理退出登录
*/
async handleLogout() {
try {
// 显示加载提示
wx.showLoading({
title: '退出中...'
})
// 调用登出API
await authService.logout()
// 清除本地认证信息
auth.clearAuth()
// 调用全局登出方法
const app = getApp()
if (app && app.logout) {
app.logout()
}
// 隐藏加载提示
wx.hideLoading()
// 显示成功提示
wx.showToast({
title: '已退出登录',
icon: 'success',
duration: 2000
})
// 跳转到登录页
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1000)
} catch (error) {
console.error('退出登录错误:', error)
// 即使API调用失败也清除本地数据并跳转
auth.clearAuth()
// 调用全局登出方法
const app = getApp()
if (app && app.logout) {
app.logout()
}
wx.hideLoading()
wx.showToast({
title: '已退出登录',
icon: 'success',
duration: 2000
})
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, 1000)
}
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
this.refreshUserInfo()
wx.stopPullDownRefresh()
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: '银行管理系统',
path: '/pages/index/index'
}
}
})

View File

@@ -1,2 +1,63 @@
<!--pages/profile/profile.wxml-->
<text>pages/profile/profile.wxml</text>
<view class="profile-container">
<!-- 用户信息头部 -->
<view class="profile-header">
<view class="user-avatar">
<image src="/images/avatar.png" class="avatar-img" />
</view>
<view class="user-info">
<view class="username">{{userInfo.username || userInfo.name || '用户'}}</view>
<view class="user-role">{{userInfo.role || '管理员'}}</view>
</view>
<view class="edit-btn" bindtap="editProfile">
<text class="edit-icon">✏️</text>
</view>
</view>
<!-- 功能菜单 -->
<view class="menu-section">
<view class="menu-item" bindtap="changePassword">
<view class="menu-icon">🔒</view>
<view class="menu-text">修改密码</view>
<view class="menu-arrow">></view>
</view>
<view class="menu-item" bindtap="hardwareManagement">
<view class="menu-icon">⚙️</view>
<view class="menu-text">硬件管理</view>
<view class="menu-arrow">></view>
</view>
<view class="menu-item" bindtap="personnelManagement">
<view class="menu-icon">👥</view>
<view class="menu-text">人员管理</view>
<view class="menu-arrow">></view>
</view>
<view class="menu-item" bindtap="aboutUs">
<view class="menu-icon"></view>
<view class="menu-text">关于我们</view>
<view class="menu-arrow">></view>
</view>
</view>
<!-- 退出登录 -->
<view class="logout-section">
<button class="logout-btn" bindtap="logout">
<text class="logout-icon">🚪</text>
<text class="logout-text">退出登录</text>
</button>
</view>
<!-- 版本信息 -->
<view class="version-info">
<text class="version-text">版本 1.0.0</text>
</view>
</view>
<!-- 加载提示 -->
<view class="loading-overlay" wx:if="{{loading}}">
<view class="loading-content">
<text class="loading-text">加载中...</text>
</view>
</view>

View File

@@ -0,0 +1,172 @@
/* pages/profile/profile.wxss */
.profile-container {
min-height: 100vh;
background: #f5f5f5;
padding: 0 0 40rpx 0;
}
/* 用户信息头部 */
.profile-header {
background: #1890ff;
padding: 60rpx 40rpx 40rpx 40rpx;
display: flex;
align-items: center;
position: relative;
}
.user-avatar {
margin-right: 30rpx;
}
.avatar-img {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background: rgba(255, 255, 255, 0.2);
border: 4rpx solid rgba(255, 255, 255, 0.3);
}
.user-info {
flex: 1;
}
.username {
font-size: 36rpx;
font-weight: 600;
color: #fff;
margin-bottom: 8rpx;
}
.user-role {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
.edit-btn {
position: absolute;
top: 60rpx;
right: 40rpx;
padding: 20rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.edit-icon {
font-size: 32rpx;
color: #fff;
}
/* 功能菜单 */
.menu-section {
background: #fff;
margin: 20rpx 0;
border-radius: 16rpx;
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
padding: 32rpx 40rpx;
border-bottom: 1rpx solid #f0f0f0;
position: relative;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:active {
background: #f8f8f8;
}
.menu-icon {
font-size: 36rpx;
margin-right: 24rpx;
width: 40rpx;
text-align: center;
}
.menu-text {
flex: 1;
font-size: 32rpx;
color: #333;
}
.menu-arrow {
font-size: 28rpx;
color: #999;
}
/* 退出登录 */
.logout-section {
margin: 20rpx 0;
padding: 0 0 40rpx 0;
}
.logout-btn {
width: 100%;
height: 88rpx;
background: #fff;
border: 2rpx solid #ff4d4f;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #ff4d4f;
font-weight: 500;
margin: 0 0 0 0;
}
.logout-btn:active {
background: #fff2f0;
}
.logout-icon {
font-size: 32rpx;
margin-right: 16rpx;
}
.logout-text {
font-size: 32rpx;
}
/* 版本信息 */
.version-info {
text-align: center;
padding: 40rpx 0;
}
.version-text {
font-size: 24rpx;
color: #999;
}
/* 加载提示 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.loading-content {
background: #fff;
padding: 40rpx 60rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: 28rpx;
color: #333;
}

View File

@@ -1,5 +1,6 @@
// pages/projects/projects.js
const { apiService } = require('../../services/apiService');
const auth = require('../../utils/auth.js');
Page({
data: {
@@ -14,10 +15,26 @@ Page({
},
onLoad() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadProjects();
},
onShow() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
this.loadProjects();
},

View File

@@ -1,4 +1,6 @@
// pages/warning/warning.js
const auth = require('../../utils/auth.js');
Page({
data: {
farms: [
@@ -46,10 +48,26 @@ Page({
},
onLoad() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
console.log('日检预警页面加载');
},
onShow() {
// 检查认证状态
if (!auth.isAuthenticated()) {
wx.reLaunch({
url: '/pages/login/login'
});
return;
}
console.log('日检预警页面显示');
},

View File

@@ -44,7 +44,7 @@
},
"compileType": "miniprogram",
"libVersion": "3.10.1",
"appid": "wx1b9c7cd2d0e0bfd3",
"appid": "wxa634eb5e3f0ab518",
"projectname": "bank-mini-program",
"isGameTourist": false,
"condition": {

View File

@@ -1,5 +1,5 @@
// 银行端小程序API服务层
const API_BASE_URL = 'https://ad.ningmuyun.com';
const API_BASE_URL = 'https://ad.ningmuyun.com/bank/api';
// 获取存储的token
const getToken = () => {
@@ -95,7 +95,7 @@ const apiService = {
// 登录
login: (username, password) => {
return request({
url: '/api/auth/login',
url: '/auth/login',
method: 'POST',
data: { username, password }
});
@@ -104,7 +104,7 @@ const apiService = {
// 获取当前用户信息
getCurrentUser: () => {
return request({
url: '/api/auth/me',
url: '/auth/me',
method: 'GET'
});
},
@@ -112,9 +112,18 @@ const apiService = {
// 登出
logout: () => {
return request({
url: '/api/auth/logout',
url: '/auth/logout',
method: 'POST'
});
},
// 修改密码
changePassword: (passwordData) => {
return request({
url: '/auth/change-password',
method: 'POST',
data: passwordData
});
}
},
@@ -123,7 +132,7 @@ const apiService = {
// 获取项目列表
getList: (params = {}) => {
return request({
url: '/api/projects',
url: '/projects',
method: 'GET',
data: params
});
@@ -132,7 +141,7 @@ const apiService = {
// 获取项目详情
getById: (id) => {
return request({
url: `/api/projects/${id}`,
url: `/projects/${id}`,
method: 'GET'
});
},
@@ -140,7 +149,7 @@ const apiService = {
// 创建项目
create: (data) => {
return request({
url: '/api/projects',
url: '/projects',
method: 'POST',
data
});
@@ -149,7 +158,7 @@ const apiService = {
// 更新项目
update: (id, data) => {
return request({
url: `/api/projects/${id}`,
url: `/projects/${id}`,
method: 'PUT',
data
});
@@ -158,7 +167,7 @@ const apiService = {
// 删除项目
delete: (id) => {
return request({
url: `/api/projects/${id}`,
url: `/projects/${id}`,
method: 'DELETE'
});
},
@@ -166,7 +175,7 @@ const apiService = {
// 获取项目统计
getStats: () => {
return request({
url: '/api/projects/stats',
url: '/projects/stats',
method: 'GET'
});
}
@@ -177,7 +186,7 @@ const apiService = {
// 获取监管任务列表
getList: (params = {}) => {
return request({
url: '/api/supervision-tasks',
url: '/supervision-tasks',
method: 'GET',
data: params
});
@@ -186,7 +195,7 @@ const apiService = {
// 获取监管任务详情
getById: (id) => {
return request({
url: `/api/supervision-tasks/${id}`,
url: `/supervision-tasks/${id}`,
method: 'GET'
});
}
@@ -197,7 +206,7 @@ const apiService = {
// 获取贷款商品列表
getList: (params = {}) => {
return request({
url: '/api/loan-products',
url: '/loan-products',
method: 'GET',
data: params
});
@@ -206,7 +215,7 @@ const apiService = {
// 获取贷款商品详情
getById: (id) => {
return request({
url: `/api/loan-products/${id}`,
url: `/loan-products/${id}`,
method: 'GET'
});
},
@@ -214,9 +223,52 @@ const apiService = {
// 获取贷款商品统计
getStats: () => {
return request({
url: '/api/loan-products/stats',
url: '/loan-products/stats',
method: 'GET'
});
},
// 获取贷款商品详情(别名)
getDetail: (id) => {
return request({
url: `/loan-products/${id}`,
method: 'GET'
});
},
// 创建贷款商品
create: (data) => {
return request({
url: '/loan-products',
method: 'POST',
data: data
});
},
// 更新贷款商品
update: (id, data) => {
return request({
url: `/loan-products/${id}`,
method: 'PUT',
data: data
});
},
// 删除贷款商品(下架)
delete: (id) => {
return request({
url: `/loan-products/${id}`,
method: 'DELETE'
});
},
// 上架/下架商品
toggleStatus: (id, status) => {
return request({
url: `/loan-products/${id}/status`,
method: 'PATCH',
data: { onSaleStatus: status }
});
}
},
@@ -225,7 +277,7 @@ const apiService = {
// 获取贷款申请列表
getList: (params = {}) => {
return request({
url: '/api/loan-applications',
url: '/loan-applications',
method: 'GET',
data: params
});
@@ -234,7 +286,7 @@ const apiService = {
// 获取贷款申请详情
getById: (id) => {
return request({
url: `/api/loan-applications/${id}`,
url: `/loan-applications/${id}`,
method: 'GET'
});
},
@@ -242,7 +294,7 @@ const apiService = {
// 获取申请统计
getStats: () => {
return request({
url: '/api/loan-applications/stats',
url: '/loan-applications/stats',
method: 'GET'
});
}
@@ -253,7 +305,7 @@ const apiService = {
// 获取贷款合同列表
getList: (params = {}) => {
return request({
url: '/api/loan-contracts',
url: '/loan-contracts',
method: 'GET',
data: params
});
@@ -262,7 +314,7 @@ const apiService = {
// 获取贷款合同详情
getById: (id) => {
return request({
url: `/api/loan-contracts/${id}`,
url: `/loan-contracts/${id}`,
method: 'GET'
});
},
@@ -270,7 +322,7 @@ const apiService = {
// 获取合同统计
getStats: () => {
return request({
url: '/api/loan-contracts/stats',
url: '/loan-contracts/stats',
method: 'GET'
});
}
@@ -281,7 +333,7 @@ const apiService = {
// 获取贷款解押列表
getList: (params = {}) => {
return request({
url: '/api/loan-releases',
url: '/loan-releases',
method: 'GET',
data: params
});
@@ -290,7 +342,7 @@ const apiService = {
// 获取贷款解押详情
getById: (id) => {
return request({
url: `/api/loan-releases/${id}`,
url: `/loan-releases/${id}`,
method: 'GET'
});
},
@@ -298,7 +350,7 @@ const apiService = {
// 获取解押统计
getStats: () => {
return request({
url: '/api/loan-releases/stats',
url: '/loan-releases/stats',
method: 'GET'
});
}

View File

@@ -1,19 +1,218 @@
// 认证相关API
const request = require('../utils/request.js')
/**
* 银行端小程序认证服务
* @file authService.js
* @description 处理用户登录、登出、用户信息管理等功能
*/
const { apiService } = require('./apiService.js')
// 认证服务
const authService = {
// 用户登录
login(username, password) {
return request.post('/auth/login', {
username,
password
})
/**
* 用户登录
* @param {string} username - 用户名
* @param {string} password - 密码
* @returns {Promise} 登录结果
*/
async login(username, password) {
try {
console.log('开始登录:', username)
// 参数验证
if (!username || !password) {
throw new Error('用户名和密码不能为空')
}
if (username.length < 3) {
throw new Error('用户名至少3个字符')
}
if (password.length < 6) {
throw new Error('密码至少6个字符')
}
// 调用登录API
const response = await apiService.auth.login(username, password)
console.log('登录响应:', response)
if (response && response.success && response.data) {
return {
success: true,
token: response.data.token,
user: response.data.user,
message: response.message || '登录成功'
}
} else {
throw new Error(response.message || '登录失败')
}
} catch (error) {
console.error('登录错误:', error)
throw error
}
},
// 获取用户信息
getUserInfo() {
return request.get('/auth/userinfo')
/**
* 获取当前用户信息
* @returns {Promise} 用户信息
*/
async getUserInfo() {
try {
const response = await apiService.auth.getCurrentUser()
if (response && response.success && response.data) {
return response.data
} else {
throw new Error(response.message || '获取用户信息失败')
}
} catch (error) {
console.error('获取用户信息错误:', error)
throw error
}
},
/**
* 用户登出
* @returns {Promise} 登出结果
*/
async logout() {
try {
console.log('开始登出')
// 调用登出API
const response = await apiService.auth.logout()
console.log('登出响应:', response)
return {
success: true,
message: '登出成功'
}
} catch (error) {
console.error('登出错误:', error)
// 即使API调用失败也认为登出成功清除本地数据
return {
success: true,
message: '登出成功'
}
}
},
/**
* 刷新用户信息
* @returns {Promise} 用户信息
*/
async refreshUserInfo() {
try {
const userInfo = await this.getUserInfo()
// 更新本地存储的用户信息
wx.setStorageSync('bank_user', userInfo)
return userInfo
} catch (error) {
console.error('刷新用户信息错误:', error)
throw error
}
},
/**
* 检查登录状态
* @returns {boolean} 是否已登录
*/
isLoggedIn() {
const token = wx.getStorageSync('bank_token')
const user = wx.getStorageSync('bank_user')
return !!(token && user)
},
/**
* 获取存储的用户信息
* @returns {Object|null} 用户信息
*/
getStoredUser() {
try {
return wx.getStorageSync('bank_user') || null
} catch (error) {
console.error('获取存储用户信息错误:', error)
return null
}
},
/**
* 获取存储的token
* @returns {string|null} token
*/
getStoredToken() {
try {
return wx.getStorageSync('bank_token') || null
} catch (error) {
console.error('获取存储token错误:', error)
return null
}
},
/**
* 修改密码
* @param {Object} passwordData - 密码数据
* @param {string} passwordData.oldPassword - 旧密码
* @param {string} passwordData.newPassword - 新密码
* @returns {Promise} 修改结果
*/
async changePassword(passwordData) {
try {
console.log('开始修改密码')
const { oldPassword, newPassword } = passwordData
// 参数验证
if (!oldPassword || !newPassword) {
throw new Error('旧密码和新密码不能为空')
}
if (newPassword.length < 6) {
throw new Error('新密码至少6个字符')
}
if (oldPassword === newPassword) {
throw new Error('新密码不能与旧密码相同')
}
// 调用修改密码API
const response = await apiService.auth.changePassword({
oldPassword,
newPassword
})
console.log('修改密码响应:', response)
if (response && response.success) {
return {
success: true,
message: response.message || '密码修改成功'
}
} else {
throw new Error(response.message || '修改密码失败')
}
} catch (error) {
console.error('修改密码错误:', error)
throw error
}
},
/**
* 清除所有认证信息
*/
clearAuth() {
try {
wx.removeStorageSync('bank_token')
wx.removeStorageSync('bank_user')
console.log('认证信息已清除')
} catch (error) {
console.error('清除认证信息错误:', error)
}
}
}
module.exports = authService
module.exports = authService

View File

@@ -1,73 +1,208 @@
/**
* 银行端小程序认证工具
* @file auth.js
* @description 处理认证状态管理、token管理等功能
*/
// 认证工具类
const auth = {
// 设置token
// 存储键名
TOKEN_KEY: 'bank_token',
USER_KEY: 'bank_user',
/**
* 设置token
* @param {string} token - 认证token
*/
setToken(token) {
try {
wx.setStorageSync('bank_token', token)
if (!token) {
console.warn('设置token失败: token为空')
return false
}
wx.setStorageSync(this.TOKEN_KEY, token)
console.log('Token已保存')
return true
} catch (error) {
console.error('设置token失败:', error)
console.error('设置token错误:', error)
return false
}
},
// 获取token
/**
* 获取token
* @returns {string|null} token
*/
getToken() {
try {
return wx.getStorageSync('bank_token') || ''
return wx.getStorageSync(this.TOKEN_KEY) || null
} catch (error) {
console.error('获取token失败:', error)
return ''
}
},
// 清除token
clearToken() {
try {
wx.removeStorageSync('bank_token')
wx.removeStorageSync('bank_user')
return true
} catch (error) {
console.error('清除token失败:', error)
return false
}
},
// 设置用户信息
setUser(user) {
try {
wx.setStorageSync('bank_user', user)
return true
} catch (error) {
console.error('设置用户信息失败:', error)
return false
}
},
// 获取用户信息
getUser() {
try {
return wx.getStorageSync('bank_user') || null
} catch (error) {
console.error('获取用户信息失败:', error)
console.error('获取token错误:', error)
return null
}
},
// 检查是否已登录
/**
* 设置用户信息
* @param {Object} user - 用户信息
*/
setUser(user) {
try {
if (!user) {
console.warn('设置用户信息失败: 用户信息为空')
return false
}
wx.setStorageSync(this.USER_KEY, user)
console.log('用户信息已保存:', user.username || user.name)
return true
} catch (error) {
console.error('设置用户信息错误:', error)
return false
}
},
/**
* 获取用户信息
* @returns {Object|null} 用户信息
*/
getUser() {
try {
return wx.getStorageSync(this.USER_KEY) || null
} catch (error) {
console.error('获取用户信息错误:', error)
return null
}
},
/**
* 检查是否已认证
* @returns {boolean} 是否已认证
*/
isAuthenticated() {
const token = this.getToken()
const user = this.getUser()
return !!(token && user)
},
// 登出
logout() {
this.clearToken()
wx.reLaunch({
url: '/pages/login/login'
})
/**
* 获取用户角色
* @returns {string|null} 用户角色
*/
getUserRole() {
try {
const user = this.getUser()
return user ? user.role : null
} catch (error) {
console.error('获取用户角色错误:', error)
return null
}
},
/**
* 获取用户名
* @returns {string|null} 用户名
*/
getUsername() {
try {
const user = this.getUser()
return user ? (user.username || user.name) : null
} catch (error) {
console.error('获取用户名错误:', error)
return null
}
},
/**
* 清除认证信息
*/
clearAuth() {
try {
wx.removeStorageSync(this.TOKEN_KEY)
wx.removeStorageSync(this.USER_KEY)
console.log('认证信息已清除')
} catch (error) {
console.error('清除认证信息错误:', error)
}
},
/**
* 登出
* @param {boolean} showToast - 是否显示提示
*/
logout(showToast = true) {
try {
// 清除本地认证信息
this.clearAuth()
if (showToast) {
wx.showToast({
title: '已退出登录',
icon: 'success',
duration: 2000
})
}
// 跳转到登录页
setTimeout(() => {
wx.reLaunch({
url: '/pages/login/login'
})
}, showToast ? 1000 : 0)
} catch (error) {
console.error('登出错误:', error)
}
},
/**
* 检查token是否过期
* @returns {boolean} token是否过期
*/
isTokenExpired() {
try {
const token = this.getToken()
if (!token) return true
// 简单的token过期检查实际项目中应该解析JWT token
// 这里可以根据实际需求实现更复杂的过期检查
return false
} catch (error) {
console.error('检查token过期错误:', error)
return true
}
},
/**
* 更新用户信息
* @param {Object} newUserInfo - 新的用户信息
*/
updateUser(newUserInfo) {
try {
if (!newUserInfo) {
console.warn('更新用户信息失败: 新用户信息为空')
return false
}
const currentUser = this.getUser()
if (!currentUser) {
console.warn('更新用户信息失败: 当前用户信息不存在')
return false
}
// 合并用户信息
const updatedUser = { ...currentUser, ...newUserInfo }
this.setUser(updatedUser)
console.log('用户信息已更新')
return true
} catch (error) {
console.error('更新用户信息错误:', error)
return false
}
}
}
module.exports = auth
module.exports = auth

View File

@@ -77,7 +77,7 @@
<template #title>
<span>无纸化防疫</span>
</template>
<a-menu-item key="paperless/epidemic"><span>防疫首页</span></a-menu-item>
<!-- <a-menu-item key="paperless/epidemic"><span>防疫首页</span></a-menu-item> -->
<a-menu-item key="paperless/epidemic/epidemic-agency"><span>防疫机构管理</span></a-menu-item>
<a-menu-item key="paperless/epidemic/epidemic-record"><span>防疫记录</span></a-menu-item>
<a-menu-item key="paperless/epidemic/vaccine-management"><span>疫苗管理</span></a-menu-item>

View File

@@ -1,5 +1,4 @@
import axios from 'axios'
import { message } from 'ant-design-vue'
// 创建axios实例
const instance = axios.create({
@@ -41,26 +40,26 @@ instance.interceptors.response.use(
// 未授权清除token并跳转到登录页面
localStorage.removeItem('token')
window.location.href = '/login'
message.error('登录已过期,请重新登录')
console.error('登录已过期,请重新登录')
break
case 403:
message.error('没有权限执行此操作')
console.error('没有权限执行此操作')
break
case 404:
message.error('请求的资源不存在')
console.error('请求的资源不存在')
break
case 500:
message.error('服务器内部错误')
console.error('服务器内部错误')
break
default:
message.error(error.response.data.message || '请求失败')
console.error(error.response.data.message || '请求失败')
}
} else if (error.request) {
// 请求发出但没有收到响应
message.error('网络错误,请检查网络连接')
console.error('网络错误,请检查网络连接')
} else {
// 请求配置出错
message.error('请求配置错误')
console.error('请求配置错误')
}
return Promise.reject(error)
}
@@ -321,6 +320,40 @@ const api = {
// 删除智能主机
delete: (id) => instance.delete(`/smart-host/${id}`)
}
},
// 屠宰场管理相关API
slaughter: {
// 获取屠宰场列表
getList: (params) => instance.get('/slaughter/slaughterhouses', { params }),
// 获取单个屠宰场详情
getDetail: (id) => instance.get(`/slaughter/slaughterhouses/${id}`),
// 创建屠宰场
create: (data) => instance.post('/slaughter/slaughterhouses', data),
// 更新屠宰场
update: (id, data) => instance.put(`/slaughter/slaughterhouses/${id}`, data),
// 删除屠宰场
delete: (id) => instance.delete(`/slaughter/slaughterhouses/${id}`),
// 切换屠宰场状态
toggleStatus: (id) => instance.patch(`/slaughter/slaughterhouses/${id}/status`)
},
// 无害化登记管理相关API
harmlessRegistration: {
getList: (params) => instance.get('/harmless/list', { params }),
getDetail: (id) => instance.get(`/harmless/detail/${id}`),
create: (data) => instance.post('/harmless/create', data),
update: (id, data) => instance.put(`/harmless/update/${id}`, data),
delete: (id) => instance.delete(`/harmless/delete/${id}`)
},
// 无害化场所管理相关API
harmlessPlace: {
getList: (params) => instance.get('/harmless-place/list', { params }),
getDetail: (id) => instance.get(`/harmless-place/detail/${id}`),
create: (data) => instance.post('/harmless-place/create', data),
update: (id, data) => instance.put(`/harmless-place/update/${id}`, data),
delete: (id) => instance.delete(`/harmless-place/delete/${id}`)
}
}

View File

@@ -365,63 +365,63 @@ const stockForm = reactive({
const materialsData = ref([])
// 获取物资列表
const fetchMaterials = async () => {
try {
const params = {
keyword: searchKeyword.value,
category: categoryFilter.value,
status: statusFilter.value,
page: pagination.current,
pageSize: pagination.pageSize
}
const response = await api.warehouse.getList(params)
// 根据后端实际返回的数据结构进行调整
materialsData.value = response.data || []
pagination.total = response.total || 0
} catch (error) {
console.error('获取物资列表失败:', error)
// 如果获取失败,提供一些模拟数据以便页面可以正常显示
materialsData.value = [
{
id: '1',
code: 'M001',
name: '玉米饲料',
category: 'feed',
unit: '吨',
stockQuantity: 150,
warningQuantity: 50,
status: 'normal',
supplier: '希望饲料厂',
updateTime: '2024-04-07 10:15:00'
},
{
id: '2',
code: 'M002',
name: '牛瘟疫苗',
category: 'medicine',
unit: '盒',
stockQuantity: 20,
warningQuantity: 10,
status: 'low',
supplier: '生物制药公司',
updateTime: '2024-04-05 14:30:00'
},
{
id: '3',
code: 'M003',
name: '兽用注射器',
category: 'equipment',
unit: '个',
stockQuantity: 0,
warningQuantity: 50,
status: 'out',
supplier: '医疗器械公司',
updateTime: '2024-04-01 09:45:00'
}
]
pagination.total = materialsData.value.length
}
}
// const fetchMaterials = async () => {
// try {
// const params = {
// keyword: searchKeyword.value,
// category: categoryFilter.value,
// status: statusFilter.value,
// page: pagination.current,
// pageSize: pagination.pageSize
// }
// const response = await api.warehouse.getList(params)
// // 根据后端实际返回的数据结构进行调整
// materialsData.value = response.data || []
// pagination.total = response.total || 0
// } catch (error) {
// console.error('获取物资列表失败:', error)
// // 如果获取失败,提供一些模拟数据以便页面可以正常显示
// materialsData.value = [
// {
// id: '1',
// code: 'M001',
// name: '玉米饲料',
// category: 'feed',
// unit: '吨',
// stockQuantity: 150,
// warningQuantity: 50,
// status: 'normal',
// supplier: '希望饲料厂',
// updateTime: '2024-04-07 10:15:00'
// },
// {
// id: '2',
// code: 'M002',
// name: '牛瘟疫苗',
// category: 'medicine',
// unit: '盒',
// stockQuantity: 20,
// warningQuantity: 10,
// status: 'low',
// supplier: '生物制药公司',
// updateTime: '2024-04-05 14:30:00'
// },
// {
// id: '3',
// code: 'M003',
// name: '兽用注射器',
// category: 'equipment',
// unit: '个',
// stockQuantity: 0,
// warningQuantity: 50,
// status: 'out',
// supplier: '医疗器械公司',
// updateTime: '2024-04-01 09:45:00'
// }
// ]
// pagination.total = materialsData.value.length
// }
// }
// 表格列定义
const columns = [

View File

@@ -1,9 +1,11 @@
<template>
<div>
<h1>防疫机构管理</h1>
<div class="epidemic-agency-container">
<div class="header">
<h1>防疫机构管理</h1>
</div>
<!-- 搜索和操作栏 -->
<a-card style="margin-bottom: 16px;">
<div class="filter-section">
<div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: center;">
<a-input v-model:value="searchKeyword" placeholder="输入机构名称或编号" style="width: 250px;">
<template #prefix>
@@ -36,10 +38,10 @@
<span class="iconfont icon-tianjia"></span> 新增机构
</a-button>
</div>
</a-card>
</div>
<!-- 机构列表 -->
<a-card>
<div class="table-card">
<a-table
:columns="columns"
:data-source="agenciesData"
@@ -47,6 +49,7 @@
row-key="id"
:row-selection="{ selectedRowKeys, onChange: onSelectChange }"
:scroll="{ x: 'max-content' }"
@change="handleTableChange"
>
<!-- 操作列 -->
<template #bodyCell:action="{ record }">
@@ -57,14 +60,15 @@
</div>
</template>
</a-table>
</a-card>
</div>
<!-- 新增/编辑机构模态框 -->
<a-modal
v-model:open="isAddEditModalOpen"
:title="isEdit ? '编辑防疫机构' : '新增防疫机构'"
:footer="null"
width={600}
width="600px"
class="custom-modal"
>
<a-form
:model="currentAgency"
@@ -108,6 +112,10 @@
<a-input.TextArea v-model:value="currentAgency.address" placeholder="请输入机构地址" rows={3} />
</a-form-item>
<a-form-item label="防疫范围" name="epidemicScope" :rules="[{ required: true, message: '请输入防疫范围' }]">
<a-input.TextArea v-model:value="currentAgency.epidemicScope" placeholder="请输入防疫范围" rows={3} />
</a-form-item>
<a-form-item label="备注" name="remarks">
<a-input.TextArea v-model:value="currentAgency.remarks" placeholder="请输入备注信息" rows={2} />
</a-form-item>
@@ -124,6 +132,7 @@
v-model:open="isViewModalOpen"
title="查看防疫机构详情"
:footer="null"
class="custom-modal"
>
<div v-if="viewAgency">
<div style="margin-bottom: 16px;">
@@ -154,6 +163,10 @@
<span style="font-weight: bold; width: 120px; display: inline-block;">地址</span>
<span>{{ viewAgency.address }}</span>
</div>
<div style="margin-bottom: 16px;">
<span style="font-weight: bold; width: 120px; display: inline-block;">防疫范围</span>
<span>{{ viewAgency.epidemicScope }}</span>
</div>
<div style="margin-bottom: 16px;">
<span style="font-weight: bold; width: 120px; display: inline-block;">成立时间</span>
<span>{{ viewAgency.establishmentDate }}</span>
@@ -173,6 +186,7 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import api from '@/utils/api'
// 搜索条件
const searchKeyword = ref('')
@@ -212,6 +226,7 @@ const currentAgency = reactive({
manager: '',
phone: '',
address: '',
epidemicScope: '',
remarks: ''
})
@@ -219,68 +234,7 @@ const currentAgency = reactive({
const viewAgency = ref(null)
// 机构列表数据
const agenciesData = ref([
{
id: '1',
name: '省动物防疫中心',
code: 'EP001',
type: 'center',
level: 'provincial',
manager: '张三',
phone: '13800138001',
address: '北京市朝阳区农展馆南路5号',
establishmentDate: '2005-06-15',
remarks: '省级防疫管理机构'
},
{
id: '2',
name: '市动物防疫站',
code: 'EP002',
type: 'station',
level: 'municipal',
manager: '李四',
phone: '13800138002',
address: '北京市海淀区中关村南大街12号',
establishmentDate: '2008-09-20',
remarks: '市级防疫执行机构'
},
{
id: '3',
name: '县动物防疫站',
code: 'EP003',
type: 'station',
level: 'county',
manager: '王五',
phone: '13800138003',
address: '北京市顺义区府前中街5号',
establishmentDate: '2010-03-10',
remarks: '县级防疫执行机构'
},
{
id: '4',
name: '乡镇动物防疫诊疗所',
code: 'EP004',
type: 'clinic',
level: 'township',
manager: '赵六',
phone: '13800138004',
address: '北京市昌平区小汤山镇政府路28号',
establishmentDate: '2012-05-18',
remarks: '乡镇级防疫服务机构'
},
{
id: '5',
name: '区级动物防疫中心',
code: 'EP005',
type: 'center',
level: 'county',
manager: '孙七',
phone: '13800138005',
address: '北京市通州区运河东大街55号',
establishmentDate: '2009-11-25',
remarks: '区级防疫管理机构'
}
])
const agenciesData = ref([])
// 表格列定义
const columns = [
@@ -322,6 +276,18 @@ const columns = [
key: 'phone',
width: 120
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
ellipsis: true
},
{
title: '防疫范围',
dataIndex: 'epidemicScope',
key: 'epidemicScope',
ellipsis: true
},
{
title: '成立时间',
dataIndex: 'establishmentDate',
@@ -357,11 +323,32 @@ const getLevelText = (level) => {
return levelMap[level] || level
}
// 获取机构列表数据
const getAgenciesList = async () => {
try {
const response = await api.epidemic.agencies.getList({
keyword: searchKeyword.value,
type: typeFilter.value,
level: levelFilter.value,
page: pagination.value.current,
pageSize: pagination.value.pageSize
})
if (response.success) {
agenciesData.value = response.data.list
pagination.value.total = response.data.total
} else {
message.error(response.message || '获取机构列表失败')
}
} catch (error) {
console.error('获取机构列表失败:', error)
message.error('获取机构列表失败,请稍后重试')
}
}
// 处理搜索
const handleSearch = () => {
pagination.value.current = 1
// 这里应该调用API进行搜索现在使用模拟数据
message.success('搜索成功')
getAgenciesList()
}
// 处理重置
@@ -370,7 +357,13 @@ const handleReset = () => {
typeFilter.value = ''
levelFilter.value = ''
pagination.value.current = 1
// 这里应该重置搜索条件并重新加载数据
getAgenciesList()
}
// 处理表格分页变化
const handleTableChange = (paginationChange) => {
pagination.value = paginationChange
getAgenciesList()
}
// 处理新增
@@ -394,44 +387,64 @@ const handleEdit = (record) => {
}
// 处理查看
const handleView = (record) => {
viewAgency.value = record
isViewModalOpen.value = true
const handleView = async (record) => {
try {
const response = await api.epidemic.agencies.getDetail(record.id)
if (response.success) {
viewAgency.value = response.data
isViewModalOpen.value = true
} else {
message.error(response.message || '获取机构详情失败')
}
} catch (error) {
console.error('获取机构详情失败:', error)
message.error('获取机构详情失败,请稍后重试')
}
}
// 处理删除
const handleDelete = (id) => {
const handleDelete = async (id) => {
// 显示确认对话框
if (confirm('确定要删除该防疫机构吗?')) {
// 这里应该调用API进行删除现在使用模拟数据
const index = agenciesData.value.findIndex(item => item.id === id)
if (index !== -1) {
agenciesData.value.splice(index, 1)
message.success('删除成功')
try {
const response = await api.epidemic.agencies.delete(id)
if (response.success) {
message.success('删除成功')
getAgenciesList()
} else {
message.error(response.message || '删除失败')
}
} catch (error) {
console.error('删除机构失败:', error)
message.error('删除机构失败,请稍后重试')
}
}
}
// 处理保存
const handleSave = () => {
const handleSave = async () => {
if (formRef.value) {
formRef.value.validate().then(() => {
// 这里应该调用API进行保存现在使用模拟数据
if (isEdit.value) {
// 编辑现有记录
const index = agenciesData.value.findIndex(item => item.id === currentAgency.id)
if (index !== -1) {
agenciesData.value[index] = { ...currentAgency }
formRef.value.validate().then(async () => {
try {
let response
if (isEdit.value) {
// 编辑现有记录
response = await api.epidemic.agencies.update(currentAgency.id, currentAgency)
} else {
// 新增记录
response = await api.epidemic.agencies.create(currentAgency)
}
} else {
// 新增记录
const newAgency = { ...currentAgency }
newAgency.id = String(Date.now())
newAgency.establishmentDate = new Date().toISOString().split('T')[0]
agenciesData.value.unshift(newAgency)
if (response.success) {
isAddEditModalOpen.value = false
message.success(isEdit.value ? '编辑成功' : '新增成功')
getAgenciesList()
} else {
message.error(response.message || (isEdit.value ? '编辑失败' : '新增失败'))
}
} catch (error) {
console.error(isEdit.value ? '编辑机构失败:' : '新增机构失败:', error)
message.error(isEdit.value ? '编辑机构失败,请稍后重试' : '新增机构失败,请稍后重试')
}
isAddEditModalOpen.value = false
message.success(isEdit.value ? '编辑成功' : '新增成功')
}).catch(() => {
message.error('请检查表单数据')
})
@@ -440,16 +453,99 @@ const handleSave = () => {
// 组件挂载时初始化
onMounted(() => {
// 初始化分页总数
pagination.value.total = agenciesData.value.length
getAgenciesList()
})
</script>
<style scoped>
h1 {
font-size: 20px;
font-weight: 600;
margin-bottom: 20px;
color: #333;
}
/* 容器样式 */
.epidemic-agency-container {
padding: 24px;
/* background-color: #f0f2f5; */
min-height: 100vh;
}
/* 页面标题 */
.header {
margin-bottom: 16px;
}
.header h1 {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 0;
}
/* 过滤器和搜索区域样式 */
.filter-section {
margin-bottom: 16px;
background-color: #fff;
padding: 16px;
border-radius: 6px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
}
/* 表格卡片样式 */
.table-card {
background-color: #fff;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
padding: 16px;
}
/* 表格样式优化 */
.custom-table {
border-radius: 6px;
overflow: hidden;
}
.custom-table .ant-table-thead > tr > th {
background-color: #fafafa;
font-weight: 600;
color: #262626;
border-bottom: 1px solid #f0f0f0;
}
.custom-table .ant-table-tbody > tr:hover > td {
background-color: #f5f5f5;
}
/* 表格单元格样式 */
.table-cell {
padding: 12px 16px;
}
/* 操作按钮组样式 */
.action-group {
display: flex;
gap: 8px;
}
/* 弹窗样式优化 */
.custom-modal .ant-modal-header {
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.custom-modal .ant-modal-title {
font-size: 18px;
font-weight: 600;
color: #262626;
}
.custom-modal .ant-modal-footer {
border-top: 1px solid #f0f0f0;
}
/* 表单样式优化 */
.custom-form .ant-form-item {
margin-bottom: 16px;
}
.custom-form .ant-form-item-label {
font-weight: 500;
color: #595959;
}
</style>

View File

@@ -12,6 +12,7 @@
placeholder="搜索屠宰场名称"
style="width: 300px; margin-left: 16px;"
@search="handleSearch"
v-model:value="searchKeyword"
/>
</div>
@@ -20,13 +21,21 @@
:columns="columns"
:data-source="slaughterhouses"
row-key="id"
pagination
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
>
<template #column:action="{ record }">
<a-space>
<a-button type="link" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" danger @click="handleDelete(record.id)">删除</a-button>
<a-button type="link" @click="handleDetail(record.id)">详情</a-button>
<a-popconfirm
title="确定要删除该屠宰场吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
<a-button type="link" @click="handleDetail(record)">详情</a-button>
<a-button type="link" @click="handleToggleStatus(record)">{{ record.status === '正常' ? '暂停' : '启用' }}</a-button>
</a-space>
</template>
<template #column:status="{ text }">
@@ -36,47 +45,155 @@
</template>
</a-table>
</div>
<!-- 新增/编辑表单 -->
<a-modal
v-model:open="isModalOpen"
title="屠宰场信息"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form-model
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-form-model-item label="屠宰场名称" prop="name">
<a-input v-model="formData.name" placeholder="请输入屠宰场名称" />
</a-form-model-item>
<a-form-model-item label="地址" prop="address">
<a-input v-model="formData.address" placeholder="请输入地址" />
</a-form-model-item>
<a-form-model-item label="联系人" prop="contactPerson">
<a-input v-model="formData.contactPerson" placeholder="请输入联系人" />
</a-form-model-item>
<a-form-model-item label="联系电话" prop="contactPhone">
<a-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
</a-form-model-item>
<a-form-model-item label="许可证号" prop="licenseNumber">
<a-input v-model="formData.licenseNumber" placeholder="请输入许可证号" />
</a-form-model-item>
<a-form-model-item label="状态" prop="status">
<a-select v-model="formData.status">
<a-select-option value="正常">正常</a-select-option>
<a-select-option value="暂停营业">暂停营业</a-select-option>
</a-select>
</a-form-model-item>
</a-form-model>
</a-modal>
<!-- 详情弹窗 -->
<a-modal
v-model:open="isDetailModalOpen"
title="屠宰场详情"
@cancel="handleDetailCancel"
>
<div class="detail-container">
<div class="detail-row">
<span class="detail-label">屠宰场名称</span>
<span class="detail-value">{{ detailData.name }}</span>
</div>
<div class="detail-row">
<span class="detail-label">地址</span>
<span class="detail-value">{{ detailData.address }}</span>
</div>
<div class="detail-row">
<span class="detail-label">联系人</span>
<span class="detail-value">{{ detailData.contactPerson }}</span>
</div>
<div class="detail-row">
<span class="detail-label">联系电话</span>
<span class="detail-value">{{ detailData.contactPhone }}</span>
</div>
<div class="detail-row">
<span class="detail-label">许可证号</span>
<span class="detail-value">{{ detailData.licenseNumber }}</span>
</div>
<div class="detail-row">
<span class="detail-label">状态</span>
<span class="detail-value"><a-tag :color="detailData.status === '正常' ? 'green' : 'red'">
{{ detailData.status }}
</a-tag></span>
</div>
<div class="detail-row">
<span class="detail-label">创建时间</span>
<span class="detail-value">{{ detailData.createTime }}</span>
</div>
<div class="detail-row">
<span class="detail-label">更新时间</span>
<span class="detail-value">{{ detailData.updateTime }}</span>
</div>
</div>
</a-modal>
</div>
</template>
<script>
import { ref } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import api from '@/utils/api'
export default {
setup() {
// 模拟数据
const slaughterhouses = ref([
{
id: '1',
name: '东方屠宰场',
address: '北京市朝阳区东方路123号',
contactPerson: '张三',
contactPhone: '13800138001',
licenseNumber: 'SL20230001',
status: '正常',
createTime: '2023-01-15'
},
{
id: '2',
name: '南方屠宰场',
address: '北京市海淀区南大街45号',
contactPerson: '李四',
contactPhone: '13900139002',
licenseNumber: 'SL20230002',
status: '正常',
createTime: '2023-02-20'
},
{
id: '3',
name: '北方屠宰场',
address: '北京市西城区北大街67号',
contactPerson: '王五',
contactPhone: '13700137003',
licenseNumber: 'SL20230003',
status: '暂停营业',
createTime: '2023-03-10'
}
])
// 数据列表
const slaughterhouses = ref([])
// 加载状态
const loading = ref(false)
// 搜索关键词
const searchKeyword = ref('')
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
})
// 模态框状态
const isModalOpen = ref(false)
const isDetailModalOpen = ref(false)
// 表单引用
const formRef = ref(null)
// 表单数据
const formData = reactive({
id: '',
name: '',
address: '',
contactPerson: '',
contactPhone: '',
licenseNumber: '',
status: '正常'
})
// 详情数据
const detailData = reactive({})
// 表单验证规则
const formRules = {
name: [
{ required: true, message: '请输入屠宰场名称', trigger: 'blur' }
],
address: [
{ required: true, message: '请输入地址', trigger: 'blur' }
],
contactPerson: [
{ required: true, message: '请输入联系人', trigger: 'blur' }
],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' }
],
licenseNumber: [
{ required: true, message: '请输入许可证号', trigger: 'blur' }
]
}
// 表格列配置
const columns = [
@@ -109,7 +226,7 @@ export default {
title: '状态',
dataIndex: 'status',
key: 'status',
scopedSlots: { customRender: 'status' }
slots: { customRender: 'status' }
},
{
title: '创建时间',
@@ -119,48 +236,191 @@ export default {
{
title: '操作',
key: 'action',
scopedSlots: { customRender: 'action' }
slots: { customRender: 'action' }
}
]
// 获取屠宰场列表
const fetchSlaughterhouses = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchKeyword.value
}
const response = await api.slaughter.getList(params)
if (response && response.data) {
slaughterhouses.value = response.data.list || []
pagination.total = response.data.total || 0
} else {
slaughterhouses.value = []
pagination.total = 0
}
} catch (error) {
message.error('获取屠宰场列表失败')
console.error('获取屠宰场列表失败:', error)
} finally {
loading.value = false
}
}
// 处理搜索
const handleSearch = (value) => {
console.log('搜索:', value)
// 实际项目中这里应该调用API进行搜索
const handleSearch = async () => {
pagination.current = 1
await fetchSlaughterhouses()
}
// 处理表格变化
const handleTableChange = async (paginationConfig) => {
Object.assign(pagination, paginationConfig)
await fetchSlaughterhouses()
}
// 处理新增
const handleAdd = () => {
console.log('新增屠宰场')
// 实际项目中这里应该打开新增表单
// 重置表单
Object.assign(formData, {
id: '',
name: '',
address: '',
contactPerson: '',
contactPhone: '',
licenseNumber: '',
status: '正常'
})
if (formRef.value) {
formRef.value.resetFields()
}
isModalOpen.value = true
}
// 处理编辑
const handleEdit = (record) => {
console.log('编辑屠宰场:', record)
// 实际项目中这里应该打开编辑表单
const handleEdit = async (record) => {
try {
const response = await api.slaughter.getDetail(record.id)
if (response && response.data) {
Object.assign(formData, response.data)
isModalOpen.value = true
}
} catch (error) {
message.error('获取屠宰场详情失败')
console.error('获取屠宰场详情失败:', error)
}
}
// 处理删除
const handleDelete = (id) => {
console.log('删除屠宰场:', id)
// 实际项目中这里应该弹出确认框并调用API删除
const handleDelete = async (id) => {
try {
const response = await api.slaughter.delete(id)
if (response && response.code === 200) {
message.success('删除成功')
await fetchSlaughterhouses()
}
} catch (error) {
message.error('删除失败')
console.error('删除失败:', error)
}
}
// 处理查看详情
const handleDetail = (id) => {
console.log('查看屠宰场详情:', id)
// 实际项目中这里应该打开详情页面
const handleDetail = async (record) => {
try {
const response = await api.slaughter.getDetail(record.id)
if (response && response.data) {
Object.assign(detailData, response.data)
isDetailModalOpen.value = true
}
} catch (error) {
message.error('获取屠宰场详情失败')
console.error('获取屠宰场详情失败:', error)
}
}
// 处理切换状态
const handleToggleStatus = async (record) => {
try {
const response = await api.slaughter.toggleStatus(record.id)
if (response && response.code === 200) {
message.success('状态切换成功')
await fetchSlaughterhouses()
}
} catch (error) {
message.error('状态切换失败')
console.error('状态切换失败:', error)
}
}
// 表单提交
const handleOk = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
let response
if (formData.id) {
// 更新
response = await api.slaughter.update(formData.id, formData)
} else {
// 新增
response = await api.slaughter.create(formData)
}
if (response && response.code === 200) {
message.success(formData.id ? '更新成功' : '新增成功')
isModalOpen.value = false
await fetchSlaughterhouses()
}
} catch (error) {
if (error && error.errors && error.errors.length > 0) {
message.error(error.errors[0].message)
} else if (error && error.message) {
message.error(error.message)
} else {
message.error(formData.id ? '更新失败' : '新增失败')
}
console.error(formData.id ? '更新失败:' : '新增失败:', error)
}
}
// 取消表单
const handleCancel = () => {
isModalOpen.value = false
if (formRef.value) {
formRef.value.resetFields()
}
}
// 取消详情
const handleDetailCancel = () => {
isDetailModalOpen.value = false
}
// 组件挂载时获取数据
onMounted(async () => {
await fetchSlaughterhouses()
})
return {
slaughterhouses,
loading,
searchKeyword,
pagination,
isModalOpen,
isDetailModalOpen,
formRef,
formData,
detailData,
formRules,
columns,
handleSearch,
handleTableChange,
handleAdd,
handleEdit,
handleDelete,
handleDetail
handleDetail,
handleToggleStatus,
handleOk,
handleCancel,
handleDetailCancel
}
}
}

View File

@@ -9,9 +9,10 @@
<div class="toolbar">
<a-button type="primary" @click="handleAdd">新增无害化场所</a-button>
<a-input-search
placeholder="搜索场所名称"
placeholder="搜索场所名称或许可证号"
style="width: 300px; margin-left: 16px;"
@search="handleSearch"
v-model:value="searchKeyword"
/>
</div>
@@ -20,63 +21,157 @@
:columns="columns"
:data-source="harmlessPlaces"
row-key="id"
pagination
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
>
<template #column:action="{ record }">
<a-space>
<a-button type="link" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" danger @click="handleDelete(record.id)">删除</a-button>
<a-button type="link" @click="handleDetail(record.id)">详情</a-button>
<a-popconfirm
title="确定要删除该无害化场所吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
<a-button type="link" @click="handleDetail(record)">详情</a-button>
</a-space>
</template>
<template #column:status="{ text }">
<a-tag :color="text === '正常' ? 'green' : 'red'">
<a-tag :color="getStatusColor(text)">
{{ text }}
</a-tag>
</template>
</a-table>
</div>
<!-- 新增/编辑表单 -->
<a-modal
v-model:open="isModalOpen"
title="无害化场所信息"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form-model
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-form-model-item label="场所名称" prop="name">
<a-input v-model="formData.name" placeholder="请输入场所名称" />
</a-form-model-item>
<a-form-model-item label="地址" prop="address">
<a-input v-model="formData.address" placeholder="请输入地址" />
</a-form-model-item>
<a-form-model-item label="联系人" prop="contactPerson">
<a-input v-model="formData.contactPerson" placeholder="请输入联系人" />
</a-form-model-item>
<a-form-model-item label="联系电话" prop="contactPhone">
<a-input v-model="formData.contactPhone" placeholder="请输入联系电话" />
</a-form-model-item>
<a-form-model-item label="许可证号" prop="licenseNumber">
<a-input v-model="formData.licenseNumber" placeholder="请输入许可证号" />
</a-form-model-item>
<a-form-model-item label="状态" prop="status">
<a-select v-model="formData.status">
<a-select-option value="正常">正常</a-select-option>
<a-select-option value="维护中">维护中</a-select-option>
<a-select-option value="停用">停用</a-select-option>
</a-select>
</a-form-model-item>
</a-form-model>
</a-modal>
<!-- 详情弹窗 -->
<a-modal
v-model:open="isDetailModalOpen"
title="无害化场所详情"
@cancel="handleDetailCancel"
>
<div class="detail-container">
<div class="detail-row">
<span class="detail-label">场所名称</span>
<span class="detail-value">{{ detailData.name }}</span>
</div>
<div class="detail-row">
<span class="detail-label">地址</span>
<span class="detail-value">{{ detailData.address }}</span>
</div>
<div class="detail-row">
<span class="detail-label">联系人</span>
<span class="detail-value">{{ detailData.contactPerson }}</span>
</div>
<div class="detail-row">
<span class="detail-label">联系电话</span>
<span class="detail-value">{{ detailData.contactPhone }}</span>
</div>
<div class="detail-row">
<span class="detail-label">许可证号</span>
<span class="detail-value">{{ detailData.licenseNumber }}</span>
</div>
<div class="detail-row">
<span class="detail-label">状态</span>
<span class="detail-value">{{ detailData.status }}</span>
</div>
<div class="detail-row">
<span class="detail-label">创建时间</span>
<span class="detail-value">{{ detailData.createTime }}</span>
</div>
<div class="detail-row">
<span class="detail-label">更新时间</span>
<span class="detail-value">{{ detailData.updateTime }}</span>
</div>
</div>
</a-modal>
</div>
</template>
<script>
import { ref } from 'vue'
import { ref, reactive, onMounted, nextTick } from 'vue'
import api from '@/utils/api'
import { message } from 'ant-design-vue'
export default {
setup() {
// 模拟数据
const harmlessPlaces = ref([
{
id: '1',
name: '北京无害化处理中心',
address: '北京市顺义区无害化路88号',
contactPerson: '赵六',
contactPhone: '13600136001',
licenseNumber: 'HP20230001',
status: '正常',
createTime: '2023-01-20'
},
{
id: '2',
name: '天津无害化处理站',
address: '天津市滨海新区处理路56号',
contactPerson: '钱七',
contactPhone: '13500135002',
licenseNumber: 'HP20230002',
status: '正常',
createTime: '2023-02-25'
},
{
id: '3',
name: '河北无害化处理厂',
address: '河北省廊坊市大厂县处理路34号',
contactPerson: '孙八',
contactPhone: '13400134003',
licenseNumber: 'HP20230003',
status: '维护中',
createTime: '2023-03-15'
}
])
// 响应式数据
const harmlessPlaces = ref([])
const loading = ref(false)
const searchKeyword = ref('')
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
})
// 模态框状态
const isModalOpen = ref(false)
const isDetailModalOpen = ref(false)
const editingId = ref('')
// 表单引用和数据
const formRef = ref()
const formData = reactive({
name: '',
address: '',
contactPerson: '',
contactPhone: '',
licenseNumber: '',
status: '正常'
})
// 详情数据
const detailData = reactive({})
// 表单验证规则
const formRules = {
name: [{ required: true, message: '请输入场所名称', trigger: 'blur' }],
address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
licenseNumber: [{ required: true, message: '请输入许可证号', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
// 表格列配置
const columns = [
@@ -123,44 +218,200 @@ export default {
}
]
// 获取状态对应的颜色
const getStatusColor = (status) => {
const colorMap = {
'正常': 'green',
'维护中': 'orange',
'停用': 'red'
}
return colorMap[status] || 'default'
}
// 获取无害化场所列表
const fetchHarmlessPlaces = async () => {
try {
loading.value = true
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchKeyword.value
}
const res = await api.harmlessPlace.getList(params)
if (res.code === 200) {
harmlessPlaces.value = res.data.list || []
pagination.total = res.data.total || 0
} else {
message.error(res.message || '获取无害化场所列表失败')
}
} catch (error) {
console.error('获取无害化场所列表失败:', error)
message.error('获取无害化场所列表失败')
} finally {
loading.value = false
}
}
// 处理搜索
const handleSearch = (value) => {
console.log('搜索:', value)
// 实际项目中这里应该调用API进行搜索
const handleSearch = async (value) => {
searchKeyword.value = value || ''
pagination.current = 1
await fetchHarmlessPlaces()
}
// 处理表格分页变化
const handleTableChange = async (pagination) => {
Object.assign(pagination, pagination)
await fetchHarmlessPlaces()
}
// 重置表单
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields()
}
Object.assign(formData, {
name: '',
address: '',
contactPerson: '',
contactPhone: '',
licenseNumber: '',
status: '正常'
})
editingId.value = ''
}
// 处理新增
const handleAdd = () => {
console.log('新增无害化场所')
// 实际项目中这里应该打开新增表单
resetForm()
isModalOpen.value = true
}
// 处理编辑
const handleEdit = (record) => {
console.log('编辑无害化场所:', record)
// 实际项目中这里应该打开编辑表单
const handleEdit = async (record) => {
resetForm()
editingId.value = record.id
// 填充表单数据
Object.assign(formData, {
name: record.name,
address: record.address,
contactPerson: record.contactPerson,
contactPhone: record.contactPhone,
licenseNumber: record.licenseNumber,
status: record.status
})
await nextTick()
isModalOpen.value = true
}
// 处理删除
const handleDelete = (id) => {
console.log('删除无害化场所:', id)
// 实际项目中这里应该弹出确认框并调用API删除
const handleDelete = async (id) => {
try {
loading.value = true
const res = await api.harmlessPlace.delete(id)
if (res.code === 200) {
message.success('删除成功')
await fetchHarmlessPlaces()
} else {
message.error(res.message || '删除失败')
}
} catch (error) {
console.error('删除无害化场所失败:', error)
message.error('删除失败')
} finally {
loading.value = false
}
}
// 处理查看详情
const handleDetail = (id) => {
console.log('查看无害化场所详情:', id)
// 实际项目中这里应该打开详情页面
const handleDetail = async (record) => {
try {
const res = await api.harmlessPlace.getDetail(record.id)
if (res.code === 200) {
Object.assign(detailData, res.data)
isDetailModalOpen.value = true
} else {
message.error(res.message || '获取详情失败')
}
} catch (error) {
console.error('获取无害化场所详情失败:', error)
message.error('获取详情失败')
}
}
// 表单提交
const handleOk = async () => {
try {
if (!formRef.value) return
await formRef.value.validate()
loading.value = true
let res
if (editingId.value) {
// 更新
res = await api.harmlessPlace.update(editingId.value, formData)
} else {
// 新增
res = await api.harmlessPlace.create(formData)
}
if (res.code === 200) {
message.success(editingId.value ? '更新成功' : '创建成功')
isModalOpen.value = false
await fetchHarmlessPlaces()
} else {
message.error(res.message || (editingId.value ? '更新失败' : '创建失败'))
}
} catch (error) {
console.error(editingId.value ? '更新无害化场所失败' : '创建无害化场所失败', error)
message.error(editingId.value ? '更新失败' : '创建失败')
} finally {
loading.value = false
}
}
// 处理取消
const handleCancel = () => {
isModalOpen.value = false
resetForm()
}
// 处理详情弹窗取消
const handleDetailCancel = () => {
isDetailModalOpen.value = false
}
// 组件挂载时获取数据
onMounted(() => {
fetchHarmlessPlaces()
})
return {
harmlessPlaces,
loading,
searchKeyword,
pagination,
isModalOpen,
isDetailModalOpen,
formRef,
formData,
detailData,
formRules,
columns,
getStatusColor,
handleSearch,
handleTableChange,
handleAdd,
handleEdit,
handleDelete,
handleDetail
handleDetail,
handleOk,
handleCancel,
handleDetailCancel
}
}
}
@@ -186,4 +437,24 @@ export default {
padding: 24px;
border-radius: 2px;
}
.detail-container {
padding: 16px 0;
}
.detail-row {
margin-bottom: 16px;
line-height: 1.8;
}
.detail-label {
display: inline-block;
width: 100px;
color: rgba(0, 0, 0, 0.45);
}
.detail-value {
display: inline-block;
color: rgba(0, 0, 0, 0.85);
}
</style>

View File

@@ -11,11 +11,13 @@
<a-range-picker
style="width: 300px; margin-left: 16px;"
@change="handleDateChange"
v-model:value="dateRange"
/>
<a-input-search
placeholder="搜索登记编号"
style="width: 300px; margin-left: 16px;"
@search="handleSearch"
v-model:value="searchKeyword"
/>
</div>
@@ -24,13 +26,20 @@
:columns="columns"
:data-source="harmlessRegistrations"
row-key="id"
pagination
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
>
<template #column:action="{ record }">
<a-space>
<a-button type="link" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" danger @click="handleDelete(record.id)">删除</a-button>
<a-button type="link" @click="handleDetail(record.id)">详情</a-button>
<a-popconfirm
title="确定要删除该无害化登记吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
<a-button type="link" @click="handleDetail(record)">详情</a-button>
</a-space>
</template>
<template #column:status="{ text }">
@@ -40,56 +49,194 @@
</template>
</a-table>
</div>
<!-- 新增/编辑表单 -->
<a-modal
v-model:open="isModalOpen"
title="无害化登记信息"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form-model
ref="formRef"
:model="formData"
:rules="formRules"
layout="vertical"
>
<a-form-model-item label="登记编号" prop="registrationNumber">
<a-input v-model="formData.registrationNumber" placeholder="请输入登记编号" />
</a-form-model-item>
<a-form-model-item label="动物类型" prop="animalType">
<a-input v-model="formData.animalType" placeholder="请输入动物类型" />
</a-form-model-item>
<a-form-model-item label="数量" prop="quantity">
<a-input-number v-model="formData.quantity" placeholder="请输入数量" min="1" />
</a-form-model-item>
<a-form-model-item label="原因" prop="reason">
<a-input v-model="formData.reason" placeholder="请输入原因" />
</a-form-model-item>
<a-form-model-item label="处理方式" prop="processingMethod">
<a-input v-model="formData.processingMethod" placeholder="请输入处理方式" />
</a-form-model-item>
<a-form-model-item label="处理场所" prop="processingPlace">
<a-input v-model="formData.processingPlace" placeholder="请输入处理场所" />
</a-form-model-item>
<a-form-model-item label="处理日期" prop="processingDate">
<a-date-picker v-model="formData.processingDate" placeholder="请选择处理日期" />
</a-form-model-item>
<a-form-model-item label="登记人" prop="registrant">
<a-input v-model="formData.registrant" placeholder="请输入登记人" />
</a-form-model-item>
<a-form-model-item label="状态" prop="status">
<a-select v-model="formData.status">
<a-select-option value="待处理">待处理</a-select-option>
<a-select-option value="处理中">处理中</a-select-option>
<a-select-option value="已完成">已完成</a-select-option>
<a-select-option value="已取消">已取消</a-select-option>
</a-select>
</a-form-model-item>
</a-form-model>
</a-modal>
<!-- 详情弹窗 -->
<a-modal
v-model:open="isDetailModalOpen"
title="无害化登记详情"
@cancel="handleDetailCancel"
>
<div class="detail-container">
<div class="detail-row">
<span class="detail-label">登记编号</span>
<span class="detail-value">{{ detailData.registrationNumber }}</span>
</div>
<div class="detail-row">
<span class="detail-label">动物类型</span>
<span class="detail-value">{{ detailData.animalType }}</span>
</div>
<div class="detail-row">
<span class="detail-label">数量</span>
<span class="detail-value">{{ detailData.quantity }}</span>
</div>
<div class="detail-row">
<span class="detail-label">原因</span>
<span class="detail-value">{{ detailData.reason }}</span>
</div>
<div class="detail-row">
<span class="detail-label">处理方式</span>
<span class="detail-value">{{ detailData.processingMethod }}</span>
</div>
<div class="detail-row">
<span class="detail-label">处理场所</span>
<span class="detail-value">{{ detailData.processingPlace }}</span>
</div>
<div class="detail-row">
<span class="detail-label">处理日期</span>
<span class="detail-value">{{ detailData.processingDate }}</span>
</div>
<div class="detail-row">
<span class="detail-label">登记人</span>
<span class="detail-value">{{ detailData.registrant }}</span>
</div>
<div class="detail-row">
<span class="detail-label">状态</span>
<span class="detail-value"><a-tag :color="getStatusColor(detailData.status)">
{{ detailData.status }}
</a-tag></span>
</div>
<div class="detail-row">
<span class="detail-label">创建时间</span>
<span class="detail-value">{{ detailData.createTime }}</span>
</div>
<div class="detail-row">
<span class="detail-label">更新时间</span>
<span class="detail-value">{{ detailData.updateTime }}</span>
</div>
</div>
</a-modal>
</div>
</template>
<script>
import { ref } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import api from '@/utils/api'
export default {
setup() {
// 模拟数据
const harmlessRegistrations = ref([
{
id: '1',
registrationNumber: 'HR20230501',
animalType: '牛',
quantity: 10,
reason: '病死',
processingMethod: '焚烧',
processingPlace: '北京无害化处理中心',
processingDate: '2023-05-10',
registrant: '刘九',
status: '已完成',
createTime: '2023-05-09'
},
{
id: '2',
registrationNumber: 'HR20230502',
animalType: '牛',
quantity: 5,
reason: '事故死亡',
processingMethod: '深埋',
processingPlace: '天津无害化处理站',
processingDate: '2023-05-15',
registrant: '周十',
status: '处理中',
createTime: '2023-05-14'
},
{
id: '3',
registrationNumber: 'HR20230503',
animalType: '',
quantity: 3,
reason: '检疫不合格',
processingMethod: '焚烧',
processingPlace: '河北无害化处理厂',
processingDate: '2023-05-20',
registrant: '吴十一',
status: '待处理',
createTime: '2023-05-18'
}
])
// 数据列表
const harmlessRegistrations = ref([])
// 加载状态
const loading = ref(false)
// 搜索关键词
const searchKeyword = ref('')
// 日期范围
const dateRange = ref([])
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
})
// 模态框状态
const isModalOpen = ref(false)
const isDetailModalOpen = ref(false)
// 表单引用
const formRef = ref(null)
// 表单数据
const formData = reactive({
id: '',
registrationNumber: '',
animalType: '',
quantity: 1,
reason: '',
processingMethod: '',
processingPlace: '',
processingDate: null,
registrant: '',
status: '待处理'
})
// 详情数据
const detailData = reactive({})
// 表单验证规则
const formRules = {
registrationNumber: [
{ required: true, message: '请输入登记编号', trigger: 'blur' }
],
animalType: [
{ required: true, message: '请输入动物类型', trigger: 'blur' }
],
quantity: [
{ required: true, message: '请输入数量', trigger: 'blur' },
{ type: 'number', min: 1, message: '数量必须大于0', trigger: 'blur' }
],
reason: [
{ required: true, message: '请输入原因', trigger: 'blur' }
],
processingMethod: [
{ required: true, message: '请输入处理方式', trigger: 'blur' }
],
processingPlace: [
{ required: true, message: '请输入处理场所', trigger: 'blur' }
],
processingDate: [
{ required: true, message: '请选择处理日期', trigger: 'change' }
],
registrant: [
{ required: true, message: '请输入登记人', trigger: 'blur' }
]
}
// 表格列配置
const columns = [
@@ -137,7 +284,7 @@ export default {
title: '状态',
dataIndex: 'status',
key: 'status',
scopedSlots: { customRender: 'status' }
slots: { customRender: 'status' }
},
{
title: '创建时间',
@@ -147,7 +294,7 @@ export default {
{
title: '操作',
key: 'action',
scopedSlots: { customRender: 'action' }
slots: { customRender: 'action' }
}
]
@@ -162,52 +309,198 @@ export default {
return colorMap[status] || 'default'
}
// 获取无害化登记列表
const fetchHarmlessRegistrations = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchKeyword.value,
startDate: dateRange.value[0] ? dateRange.value[0].format('YYYY-MM-DD') : '',
endDate: dateRange.value[1] ? dateRange.value[1].format('YYYY-MM-DD') : ''
}
const response = await api.harmlessRegistration.getList(params)
if (response && response.data) {
harmlessRegistrations.value = response.data.list || []
pagination.total = response.data.total || 0
} else {
harmlessRegistrations.value = []
pagination.total = 0
}
} catch (error) {
message.error('获取无害化登记列表失败')
console.error('获取无害化登记列表失败:', error)
} finally {
loading.value = false
}
}
// 处理搜索
const handleSearch = (value) => {
console.log('搜索:', value)
// 实际项目中这里应该调用API进行搜索
const handleSearch = async () => {
pagination.current = 1
await fetchHarmlessRegistrations()
}
// 处理日期范围变化
const handleDateChange = (dates, dateStrings) => {
console.log('日期范围:', dates, dateStrings)
// 实际项目中这里应该根据日期范围筛选数据
const handleDateChange = async (dates) => {
dateRange.value = dates
pagination.current = 1
await fetchHarmlessRegistrations()
}
// 处理表格变化
const handleTableChange = async (paginationConfig) => {
Object.assign(pagination, paginationConfig)
await fetchHarmlessRegistrations()
}
// 处理新增
const handleAdd = () => {
console.log('新增无害化登记')
// 实际项目中这里应该打开新增表单
// 重置表单
Object.assign(formData, {
id: '',
registrationNumber: '',
animalType: '',
quantity: 1,
reason: '',
processingMethod: '',
processingPlace: '',
processingDate: null,
registrant: '',
status: '待处理'
})
if (formRef.value) {
formRef.value.resetFields()
}
isModalOpen.value = true
}
// 处理编辑
const handleEdit = (record) => {
console.log('编辑无害化登记:', record)
// 实际项目中这里应该打开编辑表单
const handleEdit = async (record) => {
try {
const response = await api.harmlessRegistration.getDetail(record.id)
if (response && response.data) {
// 转换日期格式
const data = { ...response.data }
if (data.processingDate) {
data.processingDate = new Date(data.processingDate)
}
Object.assign(formData, data)
isModalOpen.value = true
}
} catch (error) {
message.error('获取无害化登记详情失败')
console.error('获取无害化登记详情失败:', error)
}
}
// 处理删除
const handleDelete = (id) => {
console.log('删除无害化登记:', id)
// 实际项目中这里应该弹出确认框并调用API删除
const handleDelete = async (id) => {
try {
const response = await api.harmlessRegistration.delete(id)
if (response && response.code === 200) {
message.success('删除成功')
await fetchHarmlessRegistrations()
}
} catch (error) {
message.error('删除失败')
console.error('删除失败:', error)
}
}
// 处理查看详情
const handleDetail = (id) => {
console.log('查看无害化登记详情:', id)
// 实际项目中这里应该打开详情页面
const handleDetail = async (record) => {
try {
const response = await api.harmlessRegistration.getDetail(record.id)
if (response && response.data) {
Object.assign(detailData, response.data)
isDetailModalOpen.value = true
}
} catch (error) {
message.error('获取无害化登记详情失败')
console.error('获取无害化登记详情失败:', error)
}
}
// 表单提交
const handleOk = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
// 格式化日期
const submitData = { ...formData }
if (submitData.processingDate) {
submitData.processingDate = submitData.processingDate.format('YYYY-MM-DD')
}
let response
if (submitData.id) {
// 更新
response = await api.harmlessRegistration.update(submitData.id, submitData)
} else {
// 新增
response = await api.harmlessRegistration.create(submitData)
}
if (response && response.code === 200) {
message.success(submitData.id ? '更新成功' : '新增成功')
isModalOpen.value = false
await fetchHarmlessRegistrations()
}
} catch (error) {
if (error && error.errors && error.errors.length > 0) {
message.error(error.errors[0].message)
} else if (error && error.message) {
message.error(error.message)
} else {
message.error(formData.id ? '更新失败' : '新增失败')
}
console.error(formData.id ? '更新失败:' : '新增失败:', error)
}
}
// 取消表单
const handleCancel = () => {
isModalOpen.value = false
if (formRef.value) {
formRef.value.resetFields()
}
}
// 取消详情
const handleDetailCancel = () => {
isDetailModalOpen.value = false
}
// 组件挂载时获取数据
onMounted(async () => {
await fetchHarmlessRegistrations()
})
return {
harmlessRegistrations,
loading,
searchKeyword,
dateRange,
pagination,
isModalOpen,
isDetailModalOpen,
formRef,
formData,
detailData,
formRules,
columns,
getStatusColor,
handleSearch,
handleDateChange,
handleTableChange,
handleAdd,
handleEdit,
handleDelete,
handleDetail
handleDetail,
handleOk,
handleCancel,
handleDetailCancel
}
}
}

View File

@@ -240,7 +240,7 @@ export default {
}
};
// 状态相关
状态相关
const getStatusColor = (status) => {
const statusMap = {
active: 'green',

View File

@@ -50,6 +50,9 @@ app.use('/api/files', require('./routes/files'));
app.use('/api/government', require('./routes/government'));
app.use('/api/smart-earmark', require('./routes/smartEarmark'));
app.use('/api/smart-host', require('./routes/smartHost'));
app.use('/api/slaughter', require('./routes/slaughter'));
app.use('/api/harmless', require('./routes/harmless'));
app.use('/api/harmless-place', require('./routes/harmlessPlace'));
// 健康检查
app.get('/health', (req, res) => {

View File

@@ -0,0 +1,77 @@
// 清除模块缓存
function clearModuleCache() {
const modulesToClear = Object.keys(require.cache).filter(key =>
key.includes('HarmlessPlace') || key.includes('database') || key.includes('controller')
);
console.log('清除以下模块的缓存:', modulesToClear.length, '个模块');
modulesToClear.forEach(key => {
console.log('-', key);
delete require.cache[key];
});
}
// 清除缓存后再导入
clearModuleCache();
// 直接导入HarmlessPlace模型和控制器
const HarmlessPlace = require('./models/HarmlessPlace');
const harmlessPlaceController = require('./controllers/HarmlessPlaceController');
// 检查直接导入的HarmlessPlace模型
console.log('=== 直接导入的HarmlessPlace模型 ===');
console.log('类型:', typeof HarmlessPlace);
console.log('是否有findAndCountAll方法:', typeof HarmlessPlace.findAndCountAll !== 'undefined');
if (HarmlessPlace.findAndCountAll) {
console.log('findAndCountAll的类型:', typeof HarmlessPlace.findAndCountAll);
}
// 检查控制器中的getList函数
console.log('\n=== 检查控制器的getList函数 ===');
console.log('getList的类型:', typeof harmlessPlaceController.getList);
// 分析控制器中如何使用HarmlessPlace模型
// 我们需要查看控制器的源代码
const fs = require('fs');
const path = require('path');
console.log('\n=== 查看控制器的源代码 ===');
const controllerPath = path.join(__dirname, 'controllers', 'HarmlessPlaceController.js');
const controllerContent = fs.readFileSync(controllerPath, 'utf8');
// 查找导入语句
const importStatementMatch = controllerContent.match(/require\(['"]\.\.?\/models\/[^'"]+['"]\)/g);
console.log('控制器中的导入语句:', importStatementMatch || '未找到');
// 查找HarmlessPlace的使用
const harmlessPlaceUsage = controllerContent.match(/HarmlessPlace\.\w+/g);
console.log('控制器中HarmlessPlace的使用:', harmlessPlaceUsage || '未找到');
console.log('\n=== 检查控制器中实际使用的HarmlessPlace ===');
// 创建一个模拟的req和res对象
const mockReq = {
query: {
page: 1,
pageSize: 10
}
};
const mockRes = {
json: function(data) {
console.log('res.json被调用:', data);
},
status: function(code) {
console.log('res.status被调用:', code);
return this;
}
};
// 尝试从控制器中获取实际使用的HarmlessPlace模型
// 我们需要查看控制器的导入方式
const controllerModule = require('./controllers/HarmlessPlaceController');
// 如果控制器导出了其使用的模型,我们可以检查
// 但通常控制器不会导出这种依赖
console.log('控制器模块导出:', Object.keys(controllerModule));
console.log('\n所有检查完成');

View File

@@ -0,0 +1,18 @@
// 直接检查slaughter路由模块的导出内容
const slaughterRoutes = require('./routes/slaughter');
console.log('模块类型:', typeof slaughterRoutes);
console.log('是否为Express Router:', slaughterRoutes && slaughterRoutes.constructor && slaughterRoutes.constructor.name);
console.log('是否有stack属性:', 'stack' in slaughterRoutes);
if (slaughterRoutes && slaughterRoutes.stack) {
console.log('stack长度:', slaughterRoutes.stack.length);
slaughterRoutes.stack.forEach((layer, index) => {
console.log(`Layer ${index}:`, layer);
});
}
// 也检查另一个已知正常的路由模块比如auth.js
const authRoutes = require('./routes/auth');
console.log('\nauth路由模块类型:', typeof authRoutes);
console.log('auth路由模块构造函数:', authRoutes && authRoutes.constructor && authRoutes.constructor.name);

View File

@@ -0,0 +1,31 @@
// 测试HarmlessPlace模型的导出和方法
const HarmlessPlace = require('./models/HarmlessPlace');
const User = require('./models/User');
console.log('=== 检查HarmlessPlace模型 ===');
console.log('HarmlessPlace的类型:', typeof HarmlessPlace);
console.log('HarmlessPlace是否为对象:', typeof HarmlessPlace === 'object' && HarmlessPlace !== null);
console.log('HarmlessPlace是否有findAndCountAll方法:', typeof HarmlessPlace.findAndCountAll !== 'undefined');
if (HarmlessPlace.findAndCountAll) {
console.log('findAndCountAll的类型:', typeof HarmlessPlace.findAndCountAll);
}
console.log('\nHarmlessPlace对象的所有属性和方法:');
console.log(Object.keys(HarmlessPlace));
console.log('\n=== 检查User模型作为对比===');
console.log('User的类型:', typeof User);
console.log('User是否为对象:', typeof User === 'object' && User !== null);
console.log('User是否有findAndCountAll方法:', typeof User.findAndCountAll !== 'undefined');
if (User.findAndCountAll) {
console.log('findAndCountAll的类型:', typeof User.findAndCountAll);
}
console.log('\nUser对象的所有属性和方法:');
console.log(Object.keys(User));
// 检查是否存在循环引用或其他问题
console.log('\n=== 检查模型实例化 ===');
try {
console.log('尝试实例化HarmlessPlace:', new HarmlessPlace());
} catch (error) {
console.log('实例化HarmlessPlace错误:', error.message);
}

View File

@@ -38,7 +38,10 @@ async function testConnection() {
}
}
// 导出连接测试函数,但不自动执行
module.exports.testConnection = testConnection;
// 导出sequelize实例
const db = sequelize;
module.exports = sequelize;
// 再添加测试连接方法
db.testConnection = testConnection;
module.exports = db;

View File

@@ -4,6 +4,7 @@ const DB_NAME = process.env.DB_NAME || 'ningxia_zhengfu';
const DB_USER = process.env.DB_USER || 'root';
const DB_PASSWORD = process.env.DB_PASSWORD || 'aiotAiot123!';
const DB_DIALECT = process.env.DB_DIALECT || 'mysql';
// const DB_HOST = process.env.DB_HOST || '129.211.213.226';
module.exports = {
JWT_SECRET: 'your-secret-key-here', // 请在生产环境中替换为强密钥

View File

@@ -0,0 +1,199 @@
const { validationResult } = require('express-validator');
const { Op } = require('sequelize');
const HarmlessPlace = require('../models/HarmlessPlace');
// 获取无害化场所列表
exports.getList = async (req, res) => {
try {
const { page = 1, pageSize = 10, keyword = '', status = '' } = req.query;
const offset = (page - 1) * pageSize;
const where = {};
if (keyword) {
where[Op.or] = [
{ name: { [Op.like]: `%${keyword}%` } },
{ licenseNumber: { [Op.like]: `%${keyword}%` } }
];
}
if (status) {
where.status = status;
}
// 移除排序部分避免使用不存在的created_at字段
const result = await HarmlessPlace.findAndCountAll({
where,
attributes: ['id', 'name', 'address', 'contactPerson', 'contactPhone', 'licenseNumber', 'status'],
limit: parseInt(pageSize),
offset: parseInt(offset)
});
res.json({
code: 200,
message: '获取成功',
data: {
list: result.rows,
total: result.count
}
});
} catch (error) {
console.error('获取无害化场所列表失败:', error);
res.status(500).json({
code: 500,
message: '获取失败',
error: error.message
});
}
};
// 获取无害化场所详情
exports.getDetail = async (req, res) => {
try {
const { id } = req.params;
const place = await HarmlessPlace.findByPk(id);
if (!place) {
return res.status(404).json({
code: 404,
message: '无害化场所不存在'
});
}
res.json({
code: 200,
message: '获取成功',
data: place
});
} catch (error) {
console.error('获取无害化场所详情失败:', error);
res.status(500).json({
code: 500,
message: '获取失败',
error: error.message
});
}
};
// 创建无害化场所
exports.create = async (req, res) => {
try {
// 验证请求数据
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
code: 400,
message: '参数错误',
errors: errors.array()
});
}
const { name, address, contactPerson, contactPhone, licenseNumber, status } = req.body;
// 创建无害化场所
const place = await HarmlessPlace.create({
name,
address,
contactPerson,
contactPhone,
licenseNumber,
status: status || '正常'
});
res.json({
code: 200,
message: '创建成功',
data: place
});
} catch (error) {
console.error('创建无害化场所失败:', error);
res.status(500).json({
code: 500,
message: '创建失败',
error: error.message
});
}
};
// 更新无害化场所
exports.update = async (req, res) => {
try {
// 验证请求数据
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
code: 400,
message: '参数错误',
errors: errors.array()
});
}
const { id } = req.params;
const { name, address, contactPerson, contactPhone, licenseNumber, status } = req.body;
// 查找无害化场所
const place = await HarmlessPlace.findByPk(id);
if (!place) {
return res.status(404).json({
code: 404,
message: '无害化场所不存在'
});
}
// 更新无害化场所
await place.update({
name,
address,
contactPerson,
contactPhone,
licenseNumber,
status
});
res.json({
code: 200,
message: '更新成功',
data: place
});
} catch (error) {
console.error('更新无害化场所失败:', error);
res.status(500).json({
code: 500,
message: '更新失败',
error: error.message
});
}
};
// 删除无害化场所
exports.delete = async (req, res) => {
try {
const { id } = req.params;
// 查找无害化场所
const place = await HarmlessPlace.findByPk(id);
if (!place) {
return res.status(404).json({
code: 404,
message: '无害化场所不存在'
});
}
// 删除无害化场所
await place.destroy();
res.json({
code: 200,
message: '删除成功'
});
} catch (error) {
console.error('删除无害化场所失败:', error);
res.status(500).json({
code: 500,
message: '删除失败',
error: error.message
});
}
};

View File

@@ -0,0 +1,235 @@
const { validationResult } = require('express-validator');
const { Op } = require('sequelize');
const HarmlessRegistration = require('../models/HarmlessRegistration');
// 获取无害化登记列表
exports.getList = async (req, res) => {
try {
const { page = 1, pageSize = 10, keyword = '', startDate = '', endDate = '' } = req.query;
const offset = (page - 1) * pageSize;
const where = {};
if (keyword) {
where.registrationNumber = { [Op.like]: `%${keyword}%` };
}
if (startDate) {
where.processingDate = where.processingDate || {};
where.processingDate[Op.gte] = startDate;
}
if (endDate) {
where.processingDate = where.processingDate || {};
where.processingDate[Op.lte] = endDate;
}
const result = await HarmlessRegistration.findAndCountAll({
where,
limit: parseInt(pageSize),
offset: parseInt(offset),
order: [['createTime', 'DESC']]
});
res.json({
code: 200,
message: '获取成功',
data: {
list: result.rows,
total: result.count
}
});
} catch (error) {
console.error('获取无害化登记列表失败:', error);
res.status(500).json({
code: 500,
message: '获取失败',
error: error.message
});
}
};
// 获取无害化登记详情
exports.getDetail = async (req, res) => {
try {
const { id } = req.params;
const registration = await HarmlessRegistration.findByPk(id);
if (!registration) {
return res.status(404).json({
code: 404,
message: '无害化登记不存在'
});
}
res.json({
code: 200,
message: '获取成功',
data: registration
});
} catch (error) {
console.error('获取无害化登记详情失败:', error);
res.status(500).json({
code: 500,
message: '获取失败',
error: error.message
});
}
};
// 创建无害化登记
exports.create = async (req, res) => {
try {
// 验证请求参数
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
code: 400,
message: '参数验证失败',
errors: errors.array()
});
}
const { registrationNumber, animalType, quantity, reason,
processingMethod, processingPlace, processingDate, registrant, status } = req.body;
// 检查登记编号是否已存在
const existingRegistration = await HarmlessRegistration.findOne({
where: { registrationNumber }
});
if (existingRegistration) {
return res.status(400).json({
code: 400,
message: '登记编号已存在'
});
}
const registration = await HarmlessRegistration.create({
registrationNumber,
animalType,
quantity: parseInt(quantity),
reason,
processingMethod,
processingPlace,
processingDate,
registrant,
status: status || '待处理',
createTime: new Date(),
updateTime: new Date()
});
res.json({
code: 200,
message: '创建成功',
data: registration
});
} catch (error) {
console.error('创建无害化登记失败:', error);
res.status(500).json({
code: 500,
message: '创建失败',
error: error.message
});
}
};
// 更新无害化登记
exports.update = async (req, res) => {
try {
const { id } = req.params;
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
code: 400,
message: '参数验证失败',
errors: errors.array()
});
}
const registration = await HarmlessRegistration.findByPk(id);
if (!registration) {
return res.status(404).json({
code: 404,
message: '无害化登记不存在'
});
}
const { registrationNumber, animalType, quantity, reason,
processingMethod, processingPlace, processingDate, registrant, status } = req.body;
// 检查登记编号是否已被其他记录使用
if (registrationNumber && registrationNumber !== registration.registrationNumber) {
const existingRegistration = await HarmlessRegistration.findOne({
where: {
registrationNumber,
id: { [Op.ne]: id }
}
});
if (existingRegistration) {
return res.status(400).json({
code: 400,
message: '登记编号已存在'
});
}
}
await registration.update({
registrationNumber: registrationNumber || registration.registrationNumber,
animalType: animalType || registration.animalType,
quantity: quantity !== undefined ? parseInt(quantity) : registration.quantity,
reason: reason || registration.reason,
processingMethod: processingMethod || registration.processingMethod,
processingPlace: processingPlace || registration.processingPlace,
processingDate: processingDate || registration.processingDate,
registrant: registrant || registration.registrant,
status: status !== undefined ? status : registration.status,
updateTime: new Date()
});
res.json({
code: 200,
message: '更新成功',
data: registration
});
} catch (error) {
console.error('更新无害化登记失败:', error);
res.status(500).json({
code: 500,
message: '更新失败',
error: error.message
});
}
};
// 删除无害化登记
exports.delete = async (req, res) => {
try {
const { id } = req.params;
const registration = await HarmlessRegistration.findByPk(id);
if (!registration) {
return res.status(404).json({
code: 404,
message: '无害化登记不存在'
});
}
await registration.destroy();
res.json({
code: 200,
message: '删除成功'
});
} catch (error) {
console.error('删除无害化登记失败:', error);
res.status(500).json({
code: 500,
message: '删除失败',
error: error.message
});
}
};

View File

@@ -0,0 +1,268 @@
const { Op } = require('sequelize');
const Slaughterhouse = require('../models/Slaughterhouse');
// 查询屠宰场列表
exports.getSlaughterhouses = async (req, res) => {
try {
const { keyword, status, page = 1, pageSize = 10 } = req.query;
const where = {};
if (keyword) {
where[Op.or] = [
{ name: { [Op.like]: `%${keyword}%` } },
{ contactPerson: { [Op.like]: `%${keyword}%` } },
{ contactPhone: { [Op.like]: `%${keyword}%` } },
{ licenseNumber: { [Op.like]: `%${keyword}%` } }
];
}
if (status) {
where.status = status;
}
const { count, rows } = await Slaughterhouse.findAndCountAll({
where,
offset: (page - 1) * pageSize,
limit: parseInt(pageSize),
order: [['created_at', 'DESC']]
});
res.json({
code: 200,
data: {
list: rows,
total: count,
page: parseInt(page),
pageSize: parseInt(pageSize)
},
message: '查询成功'
});
} catch (error) {
console.error('查询屠宰场列表失败:', error);
res.status(500).json({
code: 500,
message: '服务器内部错误'
});
}
};
// 查询单个屠宰场详情
exports.getSlaughterhouseById = async (req, res) => {
try {
const { id } = req.params;
const slaughterhouse = await Slaughterhouse.findByPk(id);
if (!slaughterhouse) {
return res.status(404).json({
code: 404,
message: '屠宰场不存在'
});
}
res.json({
code: 200,
data: slaughterhouse,
message: '查询成功'
});
} catch (error) {
console.error('查询屠宰场详情失败:', error);
res.status(500).json({
code: 500,
message: '服务器内部错误'
});
}
};
// 新增屠宰场
exports.createSlaughterhouse = async (req, res) => {
try {
const { name, address, contactPerson, contactPhone, licenseNumber, status } = req.body;
// 检查名称是否重复
const existingSlaughterhouse = await Slaughterhouse.findOne({
where: { name }
});
if (existingSlaughterhouse) {
return res.status(400).json({
code: 400,
message: '该屠宰场名称已存在'
});
}
// 检查许可证号是否重复
const existingLicense = await Slaughterhouse.findOne({
where: { licenseNumber }
});
if (existingLicense) {
return res.status(400).json({
code: 400,
message: '该许可证号已存在'
});
}
const slaughterhouse = await Slaughterhouse.create({
name,
address,
contactPerson,
contactPhone,
licenseNumber,
status,
createTime: new Date(),
created_by: req.user?.id || null,
updated_by: req.user?.id || null
});
res.json({
code: 201,
data: slaughterhouse,
message: '新增成功'
});
} catch (error) {
console.error('新增屠宰场失败:', error);
res.status(500).json({
code: 500,
message: '服务器内部错误'
});
}
};
// 更新屠宰场
exports.updateSlaughterhouse = async (req, res) => {
try {
const { id } = req.params;
const { name, address, contactPerson, contactPhone, licenseNumber, status } = req.body;
const slaughterhouse = await Slaughterhouse.findByPk(id);
if (!slaughterhouse) {
return res.status(404).json({
code: 404,
message: '屠宰场不存在'
});
}
// 检查名称是否重复(排除当前屠宰场)
if (name && name !== slaughterhouse.name) {
const existingSlaughterhouse = await Slaughterhouse.findOne({
where: {
name,
id: { [Op.ne]: id }
}
});
if (existingSlaughterhouse) {
return res.status(400).json({
code: 400,
message: '该屠宰场名称已存在'
});
}
}
// 检查许可证号是否重复(排除当前屠宰场)
if (licenseNumber && licenseNumber !== slaughterhouse.licenseNumber) {
const existingLicense = await Slaughterhouse.findOne({
where: {
licenseNumber,
id: { [Op.ne]: id }
}
});
if (existingLicense) {
return res.status(400).json({
code: 400,
message: '该许可证号已存在'
});
}
}
await slaughterhouse.update({
name,
address,
contactPerson,
contactPhone,
licenseNumber,
status,
updated_by: req.user?.id || null
});
res.json({
code: 200,
data: slaughterhouse,
message: '更新成功'
});
} catch (error) {
console.error('更新屠宰场失败:', error);
res.status(500).json({
code: 500,
message: '服务器内部错误'
});
}
};
// 删除屠宰场
exports.deleteSlaughterhouse = async (req, res) => {
try {
const { id } = req.params;
const slaughterhouse = await Slaughterhouse.findByPk(id);
if (!slaughterhouse) {
return res.status(404).json({
code: 404,
message: '屠宰场不存在'
});
}
await slaughterhouse.destroy();
res.json({
code: 200,
message: '删除成功'
});
} catch (error) {
console.error('删除屠宰场失败:', error);
res.status(500).json({
code: 500,
message: '服务器内部错误'
});
}
};
// 切换屠宰场状态
exports.toggleSlaughterhouseStatus = async (req, res) => {
try {
const { id } = req.params;
const slaughterhouse = await Slaughterhouse.findByPk(id);
if (!slaughterhouse) {
return res.status(404).json({
code: 404,
message: '屠宰场不存在'
});
}
const newStatus = slaughterhouse.status === 'active' ? 'inactive' : 'active';
await slaughterhouse.update({
status: newStatus,
updated_by: req.user?.id || null
});
res.json({
code: 200,
data: slaughterhouse,
message: '状态切换成功'
});
} catch (error) {
console.error('切换屠宰场状态失败:', error);
res.status(500).json({
code: 500,
message: '服务器内部错误'
});
}
};

View File

@@ -1,5 +1,5 @@
const EpidemicAgency = require('../models/EpidemicAgency');
const { Op } = require('sequelize');
const EpidemicAgency = require('../models/EpidemicAgency');
// 查询防疫机构列表
exports.getEpidemicAgencies = async (req, res) => {
@@ -28,7 +28,7 @@ exports.getEpidemicAgencies = async (req, res) => {
where,
offset: (page - 1) * pageSize,
limit: parseInt(pageSize),
order: [['created_at', 'DESC']]
attributes: ['id', 'name', 'director', 'phone', 'address', 'email', 'type', 'status', 'establishmentDate', 'epidemicScope', 'description']
});
res.json({
@@ -81,7 +81,7 @@ exports.getEpidemicAgencyById = async (req, res) => {
// 新增防疫机构
exports.createEpidemicAgency = async (req, res) => {
try {
const { name, director, phone, address, email, type, status, establishmentDate, description } = req.body;
const { name, director, phone, address, email, type, status, establishmentDate, epidemicScope, description } = req.body;
const existingAgency = await EpidemicAgency.findOne({
where: { name }
@@ -103,6 +103,7 @@ exports.createEpidemicAgency = async (req, res) => {
type,
status,
establishmentDate,
epidemicScope,
description,
created_by: req.user?.id || null,
updated_by: req.user?.id || null
@@ -126,7 +127,7 @@ exports.createEpidemicAgency = async (req, res) => {
exports.updateEpidemicAgency = async (req, res) => {
try {
const { id } = req.params;
const { name, director, phone, address, email, type, status, establishmentDate, description } = req.body;
const { name, director, phone, address, email, type, status, establishmentDate, epidemicScope, description } = req.body;
const agency = await EpidemicAgency.findByPk(id);
@@ -163,6 +164,7 @@ exports.updateEpidemicAgency = async (req, res) => {
type,
status,
establishmentDate,
epidemicScope,
description,
updated_by: req.user?.id || null
});

View File

@@ -1,7 +1,5 @@
const { promisify } = require('util');
const jwt = require('jsonwebtoken');
const db = require('../config/database');
const util = require('util');
const User = require('../models/User');
// JWT配置
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
@@ -22,7 +20,7 @@ module.exports = async (req, res, next) => {
if (token.startsWith('mock-jwt-token-')) {
// 模拟用户数据,避免数据库查询
req.user = {
id: '1',
id: 1,
username: 'admin',
role: 'admin'
};
@@ -31,15 +29,18 @@ module.exports = async (req, res, next) => {
}
// 验证token
const decoded = await util.promisify(jwt.verify)(token, JWT_SECRET);
const decoded = jwt.verify(token, JWT_SECRET);
// 检查用户是否存在
const [user] = await db.query(
'SELECT id, username, role FROM users WHERE id = ?',
[decoded.id]
);
// 使用Sequelize模型检查用户是否存在
const user = await User.findOne({
where: {
id: decoded.id,
status: 'active'
},
attributes: ['id', 'username', 'role']
});
if (!user || user.length === 0) {
if (!user) {
return res.status(401).json({
code: 401,
message: '用户不存在或已被删除'
@@ -47,7 +48,7 @@ module.exports = async (req, res, next) => {
}
// 将用户信息添加到请求对象
req.user = user[0];
req.user = user;
next();
} catch (err) {
console.error(err);

View File

@@ -1,5 +1,5 @@
const sequelize = require('../config/database');
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database.js');
const EpidemicAgency = sequelize.define('EpidemicAgency', {
id: {
@@ -49,6 +49,11 @@ const EpidemicAgency = sequelize.define('EpidemicAgency', {
allowNull: false,
comment: '成立日期'
},
epidemicScope: {
type: DataTypes.TEXT,
allowNull: true,
comment: '防疫范围'
},
description: {
type: DataTypes.TEXT,
allowNull: true,

View File

@@ -0,0 +1,67 @@
const sequelize = require('../config/database');
const { DataTypes } = require('sequelize');
const HarmlessPlace = sequelize.define('HarmlessPlace', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '场所名称'
},
address: {
type: DataTypes.STRING(255),
allowNull: false,
comment: '地址'
},
contactPerson: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '联系人'
},
contactPhone: {
type: DataTypes.STRING(20),
allowNull: false,
comment: '联系电话'
},
licenseNumber: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '许可证号'
},
status: {
type: DataTypes.ENUM('正常', '维护中', '停用'),
allowNull: false,
defaultValue: '正常',
comment: '状态'
}
}, {
tableName: 'government_harmless_places',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
paranoid: false,
indexes: [
{
name: 'idx_name',
fields: ['name']
},
{
name: 'idx_status',
fields: ['status']
},
{
name: 'idx_license_number',
fields: ['licenseNumber'],
unique: true
}
],
comment: '无害化场所管理表'
});
module.exports = HarmlessPlace;

View File

@@ -0,0 +1,85 @@
const { DataTypes, Sequelize } = require('sequelize');
module.exports = (sequelize) => {
const HarmlessRegistration = sequelize.define('HarmlessRegistration', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false
},
registrationNumber: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
comment: '登记编号'
},
animalType: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '动物类型'
},
quantity: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '数量',
validate: {
min: 1
}
},
reason: {
type: DataTypes.TEXT,
allowNull: false,
comment: '无害化处理原因'
},
processingMethod: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '处理方式'
},
processingPlace: {
type: DataTypes.STRING(100),
allowNull: false,
comment: '处理场所'
},
processingDate: {
type: DataTypes.DATEONLY,
allowNull: false,
comment: '处理日期'
},
registrant: {
type: DataTypes.STRING(50),
allowNull: false,
comment: '登记人'
},
status: {
type: DataTypes.ENUM('待处理', '处理中', '已完成', '已取消'),
allowNull: false,
defaultValue: '待处理',
comment: '状态'
},
createTime: {
type: DataTypes.DATE,
defaultValue: Sequelize.fn('NOW'),
allowNull: false,
comment: '创建时间'
},
updateTime: {
type: DataTypes.DATE,
defaultValue: Sequelize.fn('NOW'),
allowNull: false,
comment: '更新时间'
}
}, {
tableName: 'government_harmless_registrations',
timestamps: false,
indexes: [
{ name: 'idx_registrationNumber', fields: ['registrationNumber'] },
{ name: 'idx_status', fields: ['status'] },
{ name: 'idx_processingDate', fields: ['processingDate'] }
],
comment: '无害化登记管理表'
});
return HarmlessRegistration;
};

View File

@@ -0,0 +1,78 @@
// 导入sequelize
const sequelize = require('../config/database');
const { DataTypes } = require('sequelize');
// 屠宰场数据模型
const Slaughterhouse = sequelize.define('Slaughterhouse', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '屠宰场ID'
},
name: {
type: DataTypes.STRING(100),
allowNull: false,
unique: true,
comment: '屠宰场名称'
},
address: {
type: DataTypes.STRING(255),
allowNull: false,
comment: '地址'
},
contactPerson: {
field: 'contactPerson',
type: DataTypes.STRING,
allowNull: true,
comment: '联系人'
},
contactPhone: {
field: 'contactPhone',
type: DataTypes.STRING,
allowNull: true,
comment: '联系电话'
},
licenseNumber: {
field: 'licenseNumber',
type: DataTypes.STRING,
allowNull: true,
comment: '许可证号'
},
status: {
type: DataTypes.ENUM('active', 'inactive'),
allowNull: false,
defaultValue: 'active',
comment: '状态active: 正常, inactive: 停用)'
},
createTime: {
field: 'createTime',
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间'
},
created_by: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '创建人ID'
},
updated_by: {
type: DataTypes.INTEGER,
allowNull: true,
comment: '更新人ID'
}
}, {
tableName: 'government_slaughterhouses',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
paranoid: false,
indexes: [
{ name: 'idx_name', fields: ['name'] },
{ name: 'idx_licenseNumber', fields: ['licenseNumber'] },
{ name: 'idx_status', fields: ['status'] }
]
});
module.exports = Slaughterhouse;

View File

@@ -0,0 +1,100 @@
/**
* 政府后端模型索引文件
* @file index.js
* @description 导出所有模型并建立关联关系
*/
const { sequelize } = require('../config/database');
// 导入所有模型
const User = require('./User');
const AdminStaff = require('./AdminStaff');
const Department = require('./Department');
const Position = require('./Position');
const Farmer = require('./Farmer');
const HarmlessPlace = require('./HarmlessPlace');
const HarmlessRegistration = require('./HarmlessRegistration');
const Material = require('./Material');
const Slaughterhouse = require('./Slaughterhouse');
const EpidemicAgency = require('./EpidemicAgency');
const SmartCollar = require('./SmartCollar');
const SmartEarmark = require('./SmartEarmark');
const SmartHost = require('./SmartHost');
const WarehouseTransaction = require('./WarehouseTransaction');
// 初始化所有模型
const initModels = () => {
try {
// 处理直接导出的模型
if (User && typeof User !== 'function') {
// User模型已经直接导出不需要额外处理
}
// 处理需要sequelize参数的模型
const modelsToInit = [
AdminStaff,
Department,
Position,
Farmer,
HarmlessPlace,
HarmlessRegistration,
Material,
Slaughterhouse,
EpidemicAgency,
SmartCollar,
SmartEarmark,
SmartHost,
WarehouseTransaction
];
// 初始化模型
modelsToInit.forEach(modelFactory => {
if (typeof modelFactory === 'function') {
// 调用函数获取模型实例
const model = modelFactory(sequelize);
// 将初始化后的模型赋值回原来的变量
Object.assign(module.exports, { [model.name]: model });
}
});
console.log('✅ 所有政府后端模型初始化完成');
} catch (error) {
console.error('❌ 政府后端模型初始化失败:', error);
}
};
// 初始化模型
initModels();
// 导出sequelize和所有模型
module.exports = {
sequelize,
User,
AdminStaff,
Department,
Position,
Farmer,
HarmlessPlace,
HarmlessRegistration,
Material,
Slaughterhouse,
EpidemicAgency,
SmartCollar,
SmartEarmark,
SmartHost,
WarehouseTransaction
};
// 同步所有模型到数据库(可选)
const syncModels = async (options = {}) => {
try {
await sequelize.sync(options);
console.log('✅ 政府后端所有模型已同步到数据库');
return true;
} catch (error) {
console.error('❌ 政府后端模型同步失败:', error);
return false;
}
};
// 添加syncModels到导出
module.exports.syncModels = syncModels;

View File

@@ -13,6 +13,7 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-validator": "^7.2.1",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
@@ -567,6 +568,28 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/express-validator": {
"version": "7.2.1",
"resolved": "https://registry.npmmirror.com/express-validator/-/express-validator-7.2.1.tgz",
"integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.21",
"validator": "~13.12.0"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/express-validator/node_modules/validator": {
"version": "13.12.0",
"resolved": "https://registry.npmmirror.com/validator/-/validator-13.12.0.tgz",
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",

View File

@@ -13,6 +13,7 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-validator": "^7.2.1",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",

View File

@@ -0,0 +1,44 @@
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const harmlessRegistrationController = require('../controllers/HarmlessRegistrationController');
const authMiddleware = require('../middleware/auth');
// 应用身份验证中间件
router.use(authMiddleware);
// 获取无害化登记列表
router.get('/list', harmlessRegistrationController.getList);
// 获取无害化登记详情
router.get('/detail/:id', harmlessRegistrationController.getDetail);
// 创建无害化登记
router.post('/create', [
body('registrationNumber').notEmpty().withMessage('登记编号不能为空'),
body('animalType').notEmpty().withMessage('动物类型不能为空'),
body('quantity').isInt({ min: 1 }).withMessage('数量必须大于0'),
body('reason').notEmpty().withMessage('原因不能为空'),
body('processingMethod').notEmpty().withMessage('处理方式不能为空'),
body('processingPlace').notEmpty().withMessage('处理场所不能为空'),
body('processingDate').notEmpty().withMessage('处理日期不能为空'),
body('registrant').notEmpty().withMessage('登记人不能为空')
], harmlessRegistrationController.create);
// 更新无害化登记
router.put('/update/:id', [
body('registrationNumber').optional().notEmpty().withMessage('登记编号不能为空'),
body('animalType').optional().notEmpty().withMessage('动物类型不能为空'),
body('quantity').optional().isInt({ min: 1 }).withMessage('数量必须大于0'),
body('reason').optional().notEmpty().withMessage('原因不能为空'),
body('processingMethod').optional().notEmpty().withMessage('处理方式不能为空'),
body('processingPlace').optional().notEmpty().withMessage('处理场所不能为空'),
body('processingDate').optional().notEmpty().withMessage('处理日期不能为空'),
body('registrant').optional().notEmpty().withMessage('登记人不能为空'),
body('status').optional().isIn(['待处理', '处理中', '已完成', '已取消']).withMessage('状态必须是待处理、处理中、已完成或已取消')
], harmlessRegistrationController.update);
// 删除无害化登记
router.delete('/delete/:id', harmlessRegistrationController.delete);
module.exports = router;

View File

@@ -0,0 +1,38 @@
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const harmlessPlaceController = require('../controllers/HarmlessPlaceController');
const authMiddleware = require('../middleware/auth');
// 应用身份验证中间件
router.use(authMiddleware);
// 获取无害化场所列表
router.get('/list', harmlessPlaceController.getList);
// 获取无害化场所详情
router.get('/detail/:id', harmlessPlaceController.getDetail);
// 创建无害化场所
router.post('/create', [
body('name').notEmpty().withMessage('场所名称不能为空'),
body('address').notEmpty().withMessage('地址不能为空'),
body('contactPerson').notEmpty().withMessage('联系人不能为空'),
body('contactPhone').notEmpty().withMessage('联系电话不能为空'),
body('licenseNumber').notEmpty().withMessage('许可证号不能为空')
], harmlessPlaceController.create);
// 更新无害化场所
router.put('/update/:id', [
body('name').optional().notEmpty().withMessage('场所名称不能为空'),
body('address').optional().notEmpty().withMessage('地址不能为空'),
body('contactPerson').optional().notEmpty().withMessage('联系人不能为空'),
body('contactPhone').optional().notEmpty().withMessage('联系电话不能为空'),
body('licenseNumber').optional().notEmpty().withMessage('许可证号不能为空'),
body('status').optional().isIn(['正常', '维护中', '停用']).withMessage('状态必须是正常、维护中或停用')
], harmlessPlaceController.update);
// 删除无害化场所
router.delete('/delete/:id', harmlessPlaceController.delete);
module.exports = router;

View File

@@ -0,0 +1,17 @@
const express = require('express');
const router = express.Router();
const slaughterhouseController = require('../controllers/SlaughterhouseController');
const authMiddleware = require('../middleware/auth');
// 应用认证中间件
router.use(authMiddleware);
// 屠宰场管理路由
router.get('/slaughterhouses', slaughterhouseController.getSlaughterhouses);
router.get('/slaughterhouses/:id', slaughterhouseController.getSlaughterhouseById);
router.post('/slaughterhouses', slaughterhouseController.createSlaughterhouse);
router.put('/slaughterhouses/:id', slaughterhouseController.updateSlaughterhouse);
router.delete('/slaughterhouses/:id', slaughterhouseController.deleteSlaughterhouse);
router.patch('/slaughterhouses/:id/status', slaughterhouseController.toggleSlaughterhouseStatus);
module.exports = router;

View File

@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router();
// 简单的测试路由
router.get('/test', (req, res) => {
res.json({
code: 200,
message: '测试路由工作正常',
timestamp: new Date()
});
});
module.exports = router;

View File

@@ -0,0 +1,150 @@
const mysql = require('mysql2/promise');
const createSlaughterhouseTable = async () => {
let connection = null;
try {
// 创建数据库连接
connection = await mysql.createConnection({
host: '129.211.213.226',
port: 9527,
user: 'root',
password: 'aiotAiot123!',
database: 'ningxia_zhengfu',
charset: 'utf8mb4'
});
console.log('数据库连接成功');
// SQL语句数组
const sqlStatements = [
// 删除旧表(如果存在)
'DROP TABLE IF EXISTS government_slaughterhouses;',
// 创建新表
`CREATE TABLE government_slaughterhouses (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '屠宰场ID',
name VARCHAR(100) NOT NULL UNIQUE COMMENT '屠宰场名称',
address VARCHAR(255) NOT NULL COMMENT '地址',
contactPerson VARCHAR(50) NOT NULL COMMENT '联系人',
contactPhone VARCHAR(20) NOT NULL COMMENT '联系电话',
licenseNumber VARCHAR(50) NOT NULL UNIQUE COMMENT '许可证号',
status ENUM('active', 'inactive') NOT NULL DEFAULT 'active' COMMENT '状态active: 正常, inactive: 停用)',
createTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
created_by INT NULL COMMENT '创建人ID',
updated_by INT NULL COMMENT '更新人ID',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='政府系统屠宰场表';`,
// 添加索引
'CREATE INDEX idx_name ON government_slaughterhouses (name);',
'CREATE INDEX idx_licenseNumber ON government_slaughterhouses (licenseNumber);',
'CREATE INDEX idx_status ON government_slaughterhouses (status);'
];
// 执行表创建相关SQL
for (const sql of sqlStatements) {
await connection.execute(sql);
console.log(`执行SQL成功: ${sql.substring(0, 50)}...`);
}
// 准备测试数据(使用参数化查询避免字符编码问题)
const testData = [
[
'宁夏银川市第一屠宰场',
'宁夏回族自治区银川市金凤区良田镇植物园路',
'张明',
'13800138001',
'SC1234567890123',
'active',
'2023-01-15 00:00:00',
1,
1
],
[
'宁夏石嘴山市肉类加工厂',
'宁夏回族自治区石嘴山市大武口区星海镇',
'李强',
'13900139002',
'SC1234567890124',
'active',
'2023-02-10 00:00:00',
1,
1
],
[
'宁夏吴忠市清真屠宰场',
'宁夏回族自治区吴忠市利通区金银滩镇',
'王芳',
'13700137003',
'SC1234567890125',
'active',
'2023-03-05 00:00:00',
1,
1
],
[
'宁夏固原市牲畜屠宰场',
'宁夏回族自治区固原市原州区官厅镇',
'赵伟',
'13600136004',
'SC1234567890126',
'inactive',
'2023-04-20 00:00:00',
1,
1
],
[
'宁夏中卫市肉类屠宰加工中心',
'宁夏回族自治区中卫市沙坡头区迎水桥镇',
'陈静',
'13500135005',
'SC1234567890127',
'active',
'2023-05-15 00:00:00',
1,
1
]
];
// 插入测试数据
const insertSql = `
INSERT INTO government_slaughterhouses (
name,
address,
contactPerson,
contactPhone,
licenseNumber,
status,
createTime,
created_by,
updated_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
let insertedCount = 0;
for (const data of testData) {
await connection.execute(insertSql, data);
insertedCount++;
console.log(`插入测试数据成功: ${data[0]}`);
}
// 查询插入的记录数
const [result] = await connection.execute('SELECT COUNT(*) AS total_records FROM government_slaughterhouses;');
console.log(`
成功创建屠宰场表并插入 ${result[0].total_records} 条测试数据!`);
} catch (error) {
console.error('创建屠宰场表或插入测试数据失败:', error);
} finally {
// 关闭数据库连接
if (connection) {
await connection.end();
console.log('数据库连接已关闭');
}
}
};
// 执行函数
createSlaughterhouseTable();

View File

@@ -0,0 +1,98 @@
-- 直接使用SQL创建屠宰场表并添加测试数据
USE ningxia_zhengfu;
-- 删除旧表(如果存在)
DROP TABLE IF EXISTS government_slaughterhouses;
-- 创建新表,指定字符集支持中文
CREATE TABLE government_slaughterhouses (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '屠宰场ID',
name VARCHAR(100) NOT NULL UNIQUE COMMENT '屠宰场名称',
address VARCHAR(255) NOT NULL COMMENT '地址',
contactPerson VARCHAR(50) NOT NULL COMMENT '联系人',
contactPhone VARCHAR(20) NOT NULL COMMENT '联系电话',
licenseNumber VARCHAR(50) NOT NULL UNIQUE COMMENT '许可证号',
status ENUM('active', 'inactive') NOT NULL DEFAULT 'active' COMMENT '状态active: 正常, inactive: 停用)',
createTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
created_by INT NULL COMMENT '创建人ID',
updated_by INT NULL COMMENT '更新人ID',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='政府系统屠宰场表';
-- 添加索引
CREATE INDEX idx_name ON government_slaughterhouses (name);
CREATE INDEX idx_licenseNumber ON government_slaughterhouses (licenseNumber);
CREATE INDEX idx_status ON government_slaughterhouses (status);
-- 插入测试数据
INSERT INTO government_slaughterhouses (
name,
address,
contactPerson,
contactPhone,
licenseNumber,
status,
createTime,
created_by,
updated_by
) VALUES
(
'宁夏银川市第一屠宰场',
'宁夏回族自治区银川市金凤区良田镇植物园路',
'张明',
'13800138001',
'SC1234567890123',
'active',
'2023-01-15 00:00:00',
1,
1
),
(
'宁夏石嘴山市肉类加工厂',
'宁夏回族自治区石嘴山市大武口区星海镇',
'李强',
'13900139002',
'SC1234567890124',
'active',
'2023-02-10 00:00:00',
1,
1
),
(
'宁夏吴忠市清真屠宰场',
'宁夏回族自治区吴忠市利通区金银滩镇',
'王芳',
'13700137003',
'SC1234567890125',
'active',
'2023-03-05 00:00:00',
1,
1
),
(
'宁夏固原市牲畜屠宰场',
'宁夏回族自治区固原市原州区官厅镇',
'赵伟',
'13600136004',
'SC1234567890126',
'inactive',
'2023-04-20 00:00:00',
1,
1
),
(
'宁夏中卫市肉类屠宰加工中心',
'宁夏回族自治区中卫市沙坡头区迎水桥镇',
'陈静',
'13500135005',
'SC1234567890127',
'active',
'2023-05-15 00:00:00',
1,
1
);
-- 查询插入的记录数
SELECT COUNT(*) AS total_records FROM government_slaughterhouses;

View File

@@ -1,4 +1,4 @@
const sequelize = require('../config/database');
const sequelize = require('../config/database.js');
const EpidemicAgency = require('../models/EpidemicAgency');
async function initEpidemicAgencyTable() {
@@ -33,6 +33,7 @@ async function initEpidemicAgencyTable() {
type: 'center',
status: 'active',
establishmentDate: '2010-01-15',
epidemicScope: '负责全市所有区域的动物防疫工作统筹管理和技术指导',
description: '负责全市动物防疫工作的统筹管理和技术指导'
},
{
@@ -44,6 +45,7 @@ async function initEpidemicAgencyTable() {
type: 'branch',
status: 'active',
establishmentDate: '2012-05-20',
epidemicScope: '负责东区所有街道、乡镇的动物防疫工作',
description: '负责东区范围内的动物防疫工作'
},
{
@@ -55,6 +57,7 @@ async function initEpidemicAgencyTable() {
type: 'branch',
status: 'active',
establishmentDate: '2013-03-10',
epidemicScope: '负责西区所有街道、乡镇的动物防疫工作',
description: '负责西区范围内的动物防疫工作'
},
{
@@ -66,6 +69,7 @@ async function initEpidemicAgencyTable() {
type: 'branch',
status: 'active',
establishmentDate: '2014-07-05',
epidemicScope: '负责北区所有街道、乡镇的动物防疫工作',
description: '负责北区范围内的动物防疫工作'
},
{
@@ -77,6 +81,7 @@ async function initEpidemicAgencyTable() {
type: 'branch',
status: 'active',
establishmentDate: '2015-02-28',
epidemicScope: '负责南区所有街道、乡镇的动物防疫工作',
description: '负责南区范围内的动物防疫工作'
},
{
@@ -88,6 +93,7 @@ async function initEpidemicAgencyTable() {
type: 'mobile',
status: 'active',
establishmentDate: '2016-09-15',
epidemicScope: '负责全市偏远地区、山区及突发事件的动物防疫工作',
description: '负责偏远地区和突发事件的动物防疫工作'
}
];

View File

@@ -0,0 +1,115 @@
const config = require('../config/index');
const mysql = require('mysql2/promise');
// 初始化无害化场所表
async function initHarmlessPlaceTable() {
let connection = null;
try {
// 创建数据库连接
console.log('测试数据库连接...');
connection = await mysql.createConnection({
host: config.DB_CONFIG.host,
port: config.DB_CONFIG.port,
user: config.DB_CONFIG.user,
password: config.DB_CONFIG.password,
database: config.DB_CONFIG.database
});
console.log('数据库连接成功');
// 检查表是否存在
console.log('检查无害化场所表是否存在...');
const [tables] = await connection.execute(
"SHOW TABLES LIKE 'government_harmless_places'"
);
// 如果表存在,删除它
if (tables.length > 0) {
console.log('无害化场所表已存在,删除它...');
await connection.execute('DROP TABLE government_harmless_places');
console.log('无害化场所表删除成功');
}
// 创建无害化场所表
console.log('创建无害化场所表...');
await connection.execute(`
CREATE TABLE government_harmless_places (
id VARCHAR(36) PRIMARY KEY NOT NULL,
name VARCHAR(100) NOT NULL COMMENT '场所名称',
address VARCHAR(255) NOT NULL COMMENT '地址',
contact_person VARCHAR(50) NOT NULL COMMENT '联系人',
contact_phone VARCHAR(20) NOT NULL COMMENT '联系电话',
license_number VARCHAR(50) NOT NULL UNIQUE COMMENT '许可证号',
status ENUM('正常', '维护中', '停用') NOT NULL DEFAULT '正常' COMMENT '状态',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_name (name),
INDEX idx_license_number (license_number),
INDEX idx_status (status)
) COMMENT '无害化场所管理表'
`);
console.log('无害化场所表创建成功!');
// 添加测试数据
console.log('开始添加测试数据...');
const testData = [
[
'1', '银川市无害化处理中心', '宁夏银川市金凤区科技园路88号',
'张经理', '13895112345', 'HP20240001', '正常',
'2024-06-01 10:00:00', '2024-06-01 10:00:00'
],
[
'2', '石嘴山市无害化处理站', '宁夏石嘴山市大武口区环保路56号',
'李站长', '13995123456', 'HP20240002', '正常',
'2024-06-02 11:30:00', '2024-06-02 11:30:00'
],
[
'3', '吴忠市无害化处理厂', '宁夏吴忠市利通区产业路34号',
'王厂长', '13795134567', 'HP20240003', '维护中',
'2024-06-03 09:15:00', '2024-06-10 14:20:00'
],
[
'4', '固原市无害化处理中心', '宁夏固原市原州区生态路12号',
'赵主任', '13695145678', 'HP20240004', '正常',
'2024-06-04 14:45:00', '2024-06-04 14:45:00'
],
[
'5', '中卫市无害化处理站', '宁夏中卫市沙坡头区环卫路23号',
'孙主任', '13595156789', 'HP20240005', '停用',
'2024-06-05 16:20:00', '2024-06-15 09:30:00'
]
];
// 批量插入数据
for (const row of testData) {
await connection.execute(
`INSERT INTO government_harmless_places
(id, name, address, contact_person, contact_phone, license_number, status, create_time, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
row
);
}
console.log('测试数据添加成功!');
console.log('无害化场所表初始化完成!');
} catch (error) {
console.error('初始化无害化场所表失败:', error);
throw error;
} finally {
// 关闭数据库连接
if (connection) {
await connection.end();
console.log('数据库连接已关闭');
}
}
}
// 执行初始化
if (require.main === module) {
initHarmlessPlaceTable().catch(err => {
console.error('程序执行失败:', err);
process.exit(1);
});
}
module.exports = initHarmlessPlaceTable;

View File

@@ -0,0 +1,110 @@
const config = require('../config');
const mysql = require('mysql2/promise');
// 初始化无害化登记表
async function initHarmlessRegistrationTable() {
let connection = null;
try {
// 创建数据库连接
console.log('测试数据库连接...');
connection = await mysql.createConnection({
host: config.DB_CONFIG.host,
port: config.DB_CONFIG.port,
user: config.DB_CONFIG.user,
password: config.DB_CONFIG.password,
database: config.DB_CONFIG.database
});
console.log('数据库连接成功');
// 检查表是否存在
console.log('检查无害化登记表是否存在...');
const [tables] = await connection.execute(
"SHOW TABLES LIKE 'government_harmless_registrations'"
);
// 如果表存在,删除它
if (tables.length > 0) {
console.log('无害化登记表已存在,删除它...');
await connection.execute('DROP TABLE government_harmless_registrations');
console.log('无害化登记表删除成功');
}
// 创建无害化登记表
console.log('创建无害化登记表...');
await connection.execute(`
CREATE TABLE government_harmless_registrations (
id VARCHAR(36) PRIMARY KEY NOT NULL,
registrationNumber VARCHAR(50) NOT NULL UNIQUE COMMENT '登记编号',
animalType VARCHAR(50) NOT NULL COMMENT '动物类型',
quantity INT NOT NULL COMMENT '数量',
reason TEXT NOT NULL COMMENT '无害化处理原因',
processingMethod VARCHAR(100) NOT NULL COMMENT '处理方式',
processingPlace VARCHAR(100) NOT NULL COMMENT '处理场所',
processingDate DATE NOT NULL COMMENT '处理日期',
registrant VARCHAR(50) NOT NULL COMMENT '登记人',
status ENUM('待处理', '处理中', '已完成', '已取消') NOT NULL DEFAULT '待处理' COMMENT '状态',
createTime DATETIME NOT NULL COMMENT '创建时间',
updateTime DATETIME NOT NULL COMMENT '更新时间',
INDEX idx_registrationNumber (registrationNumber),
INDEX idx_status (status),
INDEX idx_processingDate (processingDate)
) COMMENT '无害化登记管理表'
`);
console.log('无害化登记表创建成功!');
// 添加测试数据
console.log('开始添加测试数据...');
const testData = [
[
'1', 'WH20240601001', '牛', 5, '疾病死亡', '焚烧处理',
'银川市无害化处理中心', '2024-06-01', '张兽医', '已完成',
'2024-06-01 08:30:00', '2024-06-02 14:20:00'
],
[
'2', 'WH20240602002', '羊', 10, '自然灾害', '深埋处理',
'中卫市无害化处理中心', '2024-06-02', '李技术员', '处理中',
'2024-06-02 09:15:00', '2024-06-02 16:45:00'
],
[
'3', 'WH20240603003', '猪', 8, '检疫不合格', '化制处理',
'吴忠市无害化处理中心', '2024-06-03', '王检疫员', '待处理',
'2024-06-03 10:00:00', '2024-06-03 10:00:00'
],
[
'4', 'WH20240604004', '牛', 3, '意外死亡', '焚烧处理',
'石嘴山市无害化处理中心', '2024-06-04', '赵管理员', '已取消',
'2024-06-04 11:20:00', '2024-06-04 15:30:00'
],
[
'5', 'WH20240605005', '羊', 12, '疫情防控', '深埋处理',
'固原市无害化处理中心', '2024-06-05', '陈兽医', '已完成',
'2024-06-05 09:45:00', '2024-06-06 11:15:00'
]
];
// 批量插入数据
for (const row of testData) {
await connection.execute(
`INSERT INTO government_harmless_registrations
(id, registrationNumber, animalType, quantity, reason, processingMethod,
processingPlace, processingDate, registrant, status, createTime, updateTime)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
row
);
}
console.log('测试数据添加成功!添加了', testData.length, '条数据。');
} catch (error) {
console.error('初始化无害化登记表失败:', error);
} finally {
// 关闭数据库连接
if (connection) {
await connection.close();
}
}
}
// 执行初始化
initHarmlessRegistrationTable();

View File

@@ -0,0 +1,92 @@
const sequelize = require('../config/database');
const Slaughterhouse = require('../models/Slaughterhouse');
// 初始化屠宰场表并添加测试数据
const initSlaughterhouseData = async () => {
try {
// 先删除旧表,再重新创建
await Slaughterhouse.drop().catch(() => console.log('旧表不存在,跳过删除'));
await sequelize.sync({ force: true });
console.log('数据库同步成功,表已重新创建');
// 检查是否已有数据
const existingCount = await Slaughterhouse.count();
if (existingCount > 0) {
console.log(`已存在 ${existingCount} 条屠宰场数据,跳过初始化`);
return;
}
// 准备测试数据
const testData = [
{
name: '宁夏银川市第一屠宰场',
address: '宁夏回族自治区银川市金凤区良田镇植物园路',
contactPerson: '张明',
contactPhone: '13800138001',
licenseNumber: 'SC1234567890123',
status: 'active',
createTime: new Date('2023-01-15'),
created_by: 1,
updated_by: 1
},
{
name: '宁夏石嘴山市肉类加工厂',
address: '宁夏回族自治区石嘴山市大武口区星海镇',
contactPerson: '李强',
contactPhone: '13900139002',
licenseNumber: 'SC1234567890124',
status: 'active',
createTime: new Date('2023-02-10'),
created_by: 1,
updated_by: 1
},
{
name: '宁夏吴忠市清真屠宰场',
address: '宁夏回族自治区吴忠市利通区金银滩镇',
contactPerson: '王芳',
contactPhone: '13700137003',
licenseNumber: 'SC1234567890125',
status: 'active',
createTime: new Date('2023-03-05'),
created_by: 1,
updated_by: 1
},
{
name: '宁夏固原市牲畜屠宰场',
address: '宁夏回族自治区固原市原州区官厅镇',
contactPerson: '赵伟',
contactPhone: '13600136004',
licenseNumber: 'SC1234567890126',
status: 'inactive',
createTime: new Date('2023-04-20'),
created_by: 1,
updated_by: 1
},
{
name: '宁夏中卫市肉类屠宰加工中心',
address: '宁夏回族自治区中卫市沙坡头区迎水桥镇',
contactPerson: '陈静',
contactPhone: '13500135005',
licenseNumber: 'SC1234567890127',
status: 'active',
createTime: new Date('2023-05-15'),
created_by: 1,
updated_by: 1
}
];
// 批量创建测试数据
const createdSlaughterhouses = await Slaughterhouse.bulkCreate(testData);
console.log(`成功创建 ${createdSlaughterhouses.length} 条屠宰场测试数据`);
} catch (error) {
console.error('初始化屠宰场数据失败:', error);
} finally {
// 关闭数据库连接
await sequelize.close();
console.log('数据库连接已关闭');
}
};
// 执行初始化函数
initSlaughterhouseData();

View File

@@ -0,0 +1,24 @@
// 简洁地检查路由模块的基本信息
const express = require('express');
const slaughterRoutes = require('./routes/slaughter');
const authRoutes = require('./routes/auth');
console.log('=== slaughter路由模块 ===');
console.log('类型:', typeof slaughterRoutes);
console.log('构造函数:', slaughterRoutes && slaughterRoutes.constructor && slaughterRoutes.constructor.name);
console.log('是否有stack:', 'stack' in slaughterRoutes);
console.log('是否有get方法:', 'get' in slaughterRoutes);
console.log('\n=== auth路由模块 ===');
console.log('类型:', typeof authRoutes);
console.log('构造函数:', authRoutes && authRoutes.constructor && authRoutes.constructor.name);
console.log('是否有stack:', 'stack' in authRoutes);
console.log('是否有get方法:', 'get' in authRoutes);
// 创建一个新的Router实例进行比较
const newRouter = express.Router();
console.log('\n=== 新创建的Router实例 ===');
console.log('类型:', typeof newRouter);
console.log('构造函数:', newRouter && newRouter.constructor && newRouter.constructor.name);
console.log('是否有stack:', 'stack' in newRouter);
console.log('是否有get方法:', 'get' in newRouter);

View File

@@ -0,0 +1,103 @@
// 清除指定模块的缓存
function clearModuleCache() {
const modulesToClear = Object.keys(require.cache).filter(key =>
key.includes('HarmlessPlace') || key.includes('database') || key.includes('controller')
);
console.log('清除以下模块的缓存:', modulesToClear.length, '个模块');
modulesToClear.forEach(key => {
console.log('-', key);
delete require.cache[key];
});
}
// 模拟服务器启动过程
async function simulateServerStartup() {
try {
// 1. 首先清除模块缓存
clearModuleCache();
// 2. 记录加载顺序
console.log('\n=== 开始模拟服务器启动过程 ===');
// 3. 先加载数据库配置 - 注意这里使用正确的路径
console.log('\n1. 加载数据库配置...');
const sequelize = require('./config/database');
console.log('数据库实例加载完成');
console.log('sequelize的类型:', typeof sequelize);
// 4. 测试数据库连接
console.log('\n2. 测试数据库连接...');
try {
await sequelize.authenticate();
console.log('数据库连接成功');
} catch (error) {
console.error('数据库连接失败:', error.message);
}
// 5. 加载HarmlessPlace模型
console.log('\n3. 加载HarmlessPlace模型...');
const HarmlessPlace = require('./models/HarmlessPlace');
console.log('HarmlessPlace模型加载完成');
console.log('HarmlessPlace的类型:', typeof HarmlessPlace);
console.log('HarmlessPlace是否有findAndCountAll方法:', typeof HarmlessPlace.findAndCountAll !== 'undefined');
if (HarmlessPlace.findAndCountAll) {
console.log('findAndCountAll的类型:', typeof HarmlessPlace.findAndCountAll);
}
// 6. 尝试调用findAndCountAll方法
console.log('\n4. 尝试调用findAndCountAll方法...');
try {
const result = await HarmlessPlace.findAndCountAll({
limit: 10,
offset: 0
});
console.log('findAndCountAll调用成功结果:', result);
} catch (error) {
console.error('findAndCountAll调用失败:', error.message);
}
// 7. 加载控制器
console.log('\n5. 加载HarmlessPlaceController控制器...');
const harmlessPlaceController = require('./controllers/HarmlessPlaceController');
console.log('控制器加载完成');
// 8. 创建模拟的req和res对象
const mockReq = {
query: {
page: 1,
pageSize: 10
}
};
const mockRes = {
json: function(data) {
console.log('res.json被调用:', data);
},
status: function(code) {
console.log('res.status被调用:', code);
return this;
}
};
// 9. 尝试调用控制器的getList方法
console.log('\n6. 尝试调用控制器的getList方法...');
try {
await harmlessPlaceController.getList(mockReq, mockRes);
console.log('控制器getList方法调用成功');
} catch (error) {
console.error('控制器getList方法调用失败:', error.message);
console.error('错误堆栈:', error.stack);
}
console.log('\n=== 服务器启动模拟完成 ===');
} catch (error) {
console.error('模拟服务器启动过程中发生错误:', error.message);
console.error('错误堆栈:', error.stack);
}
}
// 运行模拟
console.log('开始执行服务器启动模拟测试...');
simulateServerStartup().catch(err => console.error('测试过程中出错:', err));

View File

@@ -0,0 +1,27 @@
const http = require('http');
const options = {
hostname: 'localhost',
port: 5352,
path: '/api/slaughter/slaughterhouses',
method: 'GET',
headers: {
'Authorization': 'Bearer mock-jwt-token-test',
'Content-Type': 'application/json'
}
};
const req = http.request(options, (res) => {
console.log(`状态码: ${res.statusCode}`);
console.log(`响应头: ${JSON.stringify(res.headers)}`);
res.on('data', (d) => {
process.stdout.write(d);
});
});
req.on('error', (error) => {
console.error(error);
});
req.end();

View File

@@ -0,0 +1,84 @@
const http = require('http');
// 测试函数
function testRoute(path, description) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'localhost',
port: 5353,
path: path,
method: 'GET',
headers: {
'Authorization': 'Bearer mock-jwt-token-test',
'Content-Type': 'application/json'
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
resolve({
path: path,
description: description,
statusCode: res.statusCode,
headers: res.headers,
body: data
});
});
});
req.on('error', (error) => {
reject({
path: path,
description: description,
error: error.message
});
});
req.end();
});
}
// 运行所有测试
async function runTests() {
console.log('开始测试路由...\n');
try {
// 测试健康检查路由
const healthResult = await testRoute('/health', '健康检查');
console.log(`${healthResult.description} - 状态码: ${healthResult.statusCode}`);
console.log(`响应: ${healthResult.body}\n`);
// 测试测试路由
const testResult = await testRoute('/api/test/test', '测试路由');
console.log(`${testResult.description} - 状态码: ${testResult.statusCode}`);
console.log(`响应: ${testResult.body}\n`);
// 测试slaughter路由
const slaughterResult = await testRoute('/api/slaughter/slaughterhouses', 'Slaughter路由');
console.log(`${slaughterResult.description} - 状态码: ${slaughterResult.statusCode}`);
console.log(`响应: ${slaughterResult.body}\n`);
// 测试不存在的路由
const notFoundResult = await testRoute('/api/not-exist', '不存在的路由');
console.log(`${notFoundResult.description} - 状态码: ${notFoundResult.statusCode}`);
console.log(`响应: ${notFoundResult.body}\n`);
} catch (error) {
console.error('测试失败:', error);
}
}
// 等待一会儿再运行测试,给服务器启动时间
sleep(2000).then(() => {
runTests();
});
// 简单的sleep函数
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -0,0 +1,95 @@
// 测试无害化场所API
const axios = require('axios');
// 政府后端服务地址
const BASE_URL = 'http://localhost:5352/api';
// 登录获取token
async function login() {
try {
console.log('开始登录...');
const response = await axios.post(`${BASE_URL}/auth/login`, {
username: 'admin',
password: '123456'
});
console.log('登录响应:', response.data);
if (response.data.code === 200 && response.data.data && response.data.data.token) {
console.log('登录成功获取到token');
return response.data.data.token;
} else {
console.log('登录失败未获取到token');
console.log('错误信息:', response.data.message || '未知错误');
return null;
}
} catch (error) {
console.error('登录请求失败:', error.message);
if (error.response) {
console.error('错误状态码:', error.response.status);
console.error('错误数据:', error.response.data);
}
return null;
}
}
// 测试无害化场所列表API
async function testHarmlessPlaceList(token) {
try {
console.log('\n测试无害化场所列表API...');
const response = await axios.get(`${BASE_URL}/harmless-place/list`, {
headers: {
'Authorization': `Bearer ${token}`
},
params: {
page: 1,
pageSize: 10
}
});
console.log('API调用成功状态码:', response.status);
console.log('返回数据结构:', Object.keys(response.data));
console.log('无害化场所总数:', response.data.total || '未知');
if (response.data.data && Array.isArray(response.data.data)) {
console.log('返回的无害化场所列表长度:', response.data.data.length);
if (response.data.data.length > 0) {
console.log('第一条无害化场所数据:');
console.log(response.data.data[0]);
}
}
return response.data;
} catch (error) {
console.error('无害化场所列表API调用失败:', error.message);
if (error.response) {
console.error('错误状态码:', error.response.status);
console.error('错误数据:', error.response.data);
}
return null;
}
}
// 主测试函数
const runTests = async () => {
console.log('开始测试无害化场所管理API...');
try {
// 1. 登录获取token
const token = await login();
if (!token) {
console.error('无法继续测试因为未获取到有效的token');
return;
}
// 2. 测试获取无害化场所列表
await testHarmlessPlaceList(token);
console.log('\n所有测试完成');
} catch (error) {
console.error('测试过程中发生错误:', error);
}
};
// 运行测试
runTests();

View File

@@ -0,0 +1,89 @@
// 清除模块缓存
function clearModuleCache() {
const modulesToClear = Object.keys(require.cache).filter(key =>
key.includes('HarmlessPlace') || key.includes('database')
);
console.log('清除以下模块的缓存:', modulesToClear.length, '个模块');
modulesToClear.forEach(key => {
console.log('-', key);
delete require.cache[key];
});
}
// 清除缓存后再导入
clearModuleCache();
const axios = require('axios');
const HarmlessPlace = require('./models/HarmlessPlace');
console.log('=== 检查HarmlessPlace模型 ===');
console.log('HarmlessPlace的类型:', typeof HarmlessPlace);
console.log('HarmlessPlace是否为对象:', typeof HarmlessPlace === 'object' && HarmlessPlace !== null);
console.log('HarmlessPlace是否有findAndCountAll方法:', typeof HarmlessPlace.findAndCountAll !== 'undefined');
if (HarmlessPlace.findAndCountAll) {
console.log('findAndCountAll的类型:', typeof HarmlessPlace.findAndCountAll);
}
// 登录函数
async function login() {
try {
const response = await axios.post('http://localhost:3000/api/auth/login', {
username: 'admin',
password: '123456'
});
console.log('登录成功token:', response.data.data.token);
return response.data.data.token;
} catch (error) {
console.error('登录失败:', error.response ? error.response.data : error.message);
return null;
}
}
// 测试无害化场所列表API
async function testHarmlessPlaceList(token) {
try {
const response = await axios.get('http://localhost:3000/api/harmless-place/list', {
headers: {
'Authorization': `Bearer ${token}`
},
params: {
page: 1,
pageSize: 10
}
});
console.log('无害化场所列表API调用成功:', response.data);
return response.data;
} catch (error) {
console.error('无害化场所列表API调用失败:', error.message);
if (error.response) {
console.error('错误数据:', error.response.data);
}
return null;
}
}
// 主函数
async function main() {
console.log('开始测试无害化场所管理API...');
// 登录获取token
const token = await login();
if (!token) {
console.log('登录失败,无法继续测试');
return;
}
// 再次检查模型类型确保在API调用前没有被修改
console.log('\n=== API调用前再次检查HarmlessPlace模型 ===');
console.log('HarmlessPlace的类型:', typeof HarmlessPlace);
console.log('HarmlessPlace是否有findAndCountAll方法:', typeof HarmlessPlace.findAndCountAll !== 'undefined');
// 测试API
await testHarmlessPlaceList(token);
console.log('\n所有测试完成');
}
// 运行测试
main().catch(err => console.error('测试过程中出错:', err));

View File

@@ -0,0 +1,51 @@
// 测试Express应用的路由注册情况
const express = require('express');
const path = require('path');
// 创建一个简单的Express应用来测试路由
const app = express();
// 尝试加载slaughter路由
try {
const slaughterRoutes = require('./routes/slaughter');
console.log('成功加载slaughter路由模块');
// 检查路由模块的内容
console.log('路由模块导出:', typeof slaughterRoutes);
// 模拟注册路由
app.use('/api/slaughter', slaughterRoutes);
console.log('成功注册slaughter路由到/api/slaughter');
// 检查路由是否有方法
if (slaughterRoutes && slaughterRoutes.stack) {
console.log('路由处理程序数量:', slaughterRoutes.stack.length);
slaughterRoutes.stack.forEach((layer, index) => {
if (layer.route) {
console.log(`路由${index + 1}:`, layer.route.path, Object.keys(layer.route.methods));
}
});
} else {
console.log('路由模块没有stack属性可能不是Express Router实例');
}
} catch (error) {
console.error('加载slaughter路由失败:', error);
}
// 检查routes目录下的文件
const fs = require('fs');
const routesDir = path.join(__dirname, 'routes');
fs.readdir(routesDir, (err, files) => {
if (err) {
console.error('读取routes目录失败:', err);
return;
}
console.log('\nroutes目录下的文件:');
files.forEach(file => {
console.log('-', file);
// 检查文件大小,确认文件不为空
const stats = fs.statSync(path.join(routesDir, file));
console.log(` 大小: ${stats.size} 字节`);
});
});

View File

@@ -0,0 +1,44 @@
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
// 中间件
app.use(cors());
app.use(bodyParser.json());
// 简单的认证中间件,允许所有请求通过
app.use((req, res, next) => {
console.log(`接收到请求: ${req.method} ${req.path}`);
// 模拟用户数据
req.user = {
id: '1',
username: 'admin',
role: 'admin'
};
next();
});
// 加载测试路由
app.use('/api/test', require('./routes/test-route'));
// 加载slaughter路由
app.use('/api/slaughter', require('./routes/slaughter'));
// 简单的健康检查路由
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// 错误处理
app.use((err, req, res, next) => {
console.error('错误:', err);
res.status(500).json({ error: '服务器错误' });
});
// 启动服务器在不同端口
const PORT = 5353;
app.listen(PORT, () => {
console.log(`测试服务器已启动在端口 ${PORT}`);
});

View File

@@ -31,7 +31,7 @@
<a class="nav-link" href="index.html">返回首页</a>
</li>
<li class="nav-item">
<a class="btn btn-outline-light ms-2" href="https://ad.ningmuyun.com" target="_blank">
<a class="btn btn-outline-light ms-2" href="https://ad.ningmuyun.com/bank/" target="_blank">
进入系统
</a>
</li>

View File

@@ -31,7 +31,7 @@
<a class="nav-link" href="index.html">返回首页</a>
</li>
<li class="nav-item">
<a class="btn btn-outline-light ms-2" href="https://ad.ningmuyun.com" target="_blank">
<a class="btn btn-outline-light ms-2" href="https://ad.ningmuyun.com/farm/" target="_blank">
<i class="bi bi-box-arrow-up-right me-1"></i>进入系统
</a>
</li>

Some files were not shown because too many files have changed in this diff Show More