Merge branch 'master' of https://gitea.aiotagro.com/xuqiuyun/nxxmdata
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# 生产环境配置
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_API_FULL_URL=https://ad.ningmuyun.com/api
|
||||
VITE_API_FULL_URL=https://ad.ningmuyun.com/farm/api
|
||||
VITE_USE_PROXY=false
|
||||
@@ -19,13 +19,18 @@ server {
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 处理Vue Router的history模式
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
# 处理Vue Router的history模式 - 支持/farm/路径
|
||||
location /farm/ {
|
||||
try_files $uri $uri/ /farm/index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
# 根路径重定向到/farm/
|
||||
location = / {
|
||||
return 301 /farm/;
|
||||
}
|
||||
|
||||
# API代理到后端服务
|
||||
location /api/ {
|
||||
|
||||
16
admin-system/public/login.html
Normal file
16
admin-system/public/login.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>重定向中...</title>
|
||||
<script>
|
||||
// 立即重定向到正确的路径
|
||||
window.location.href = '/farm/login';
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>正在重定向到登录页面...</p>
|
||||
<p>如果没有自动跳转,请点击 <a href="/farm/login">这里</a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,7 +4,7 @@ import routes from './routes'
|
||||
|
||||
// 创建路由实例
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHistory('/farm/'),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// 如果有保存的位置,则恢复到保存的位置
|
||||
@@ -30,6 +30,12 @@ router.beforeEach(async (to, from, next) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理根路径访问,重定向到仪表盘
|
||||
if (to.path === '/') {
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果访问登录页面且已有有效token,重定向到仪表盘
|
||||
if (to.path === '/login' && userStore.token && userStore.isLoggedIn) {
|
||||
const redirectPath = to.query.redirect || '/dashboard'
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.highTemperature }}</div>
|
||||
<div class="stat-label">预警</div>
|
||||
<div class="stat-label">温度预警</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
@@ -392,7 +392,7 @@ const getAlertTypeText = (type) => {
|
||||
'movement': '异常运动预警',
|
||||
'wear': '佩戴异常预警'
|
||||
}
|
||||
return typeMap[type] || '未知预警'
|
||||
return typeMap[type] || '温度预警'
|
||||
}
|
||||
|
||||
// 获取预警类型颜色
|
||||
|
||||
@@ -204,6 +204,7 @@ import { message } from 'ant-design-vue'
|
||||
import { SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
import { loadBMapScript, createMap } from '@/utils/mapService'
|
||||
import { API_CONFIG } from '@/config/env.js'
|
||||
|
||||
// 响应式数据
|
||||
const hosts = ref([])
|
||||
@@ -312,8 +313,8 @@ const fetchData = async (showMessage = false) => {
|
||||
console.log('搜索条件:', searchValue.value.trim())
|
||||
}
|
||||
|
||||
// 调用API获取智能主机数据
|
||||
const apiUrl = `/api/smart-devices/hosts?${params}`
|
||||
// 调用API获取智能主机数据(使用环境配置的基础URL,兼容生产 /farm/api 前缀)
|
||||
const apiUrl = `${API_CONFIG.baseUrl}/smart-devices/hosts?${params}`
|
||||
console.log('API请求URL:', apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
@@ -610,8 +611,8 @@ const exportData = async () => {
|
||||
console.log('导出搜索条件:', searchValue.value.trim())
|
||||
}
|
||||
|
||||
// 调用API获取所有智能主机数据
|
||||
const apiUrl = `/api/smart-devices/hosts?${params}`
|
||||
// 调用API获取所有智能主机数据(使用环境配置的基础URL,兼容生产 /farm/api 前缀)
|
||||
const apiUrl = `${API_CONFIG.baseUrl}/smart-devices/hosts?${params}`
|
||||
console.log('导出API请求URL:', apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
|
||||
@@ -7,8 +7,22 @@ export default defineConfig(({ mode }) => {
|
||||
// 加载环境变量
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
// 自定义重定向插件
|
||||
const redirectPlugin = () => {
|
||||
return {
|
||||
name: 'redirect-plugin',
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/login', (req, res, next) => {
|
||||
res.writeHead(302, { Location: '/farm/login' })
|
||||
res.end()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
plugins: [vue()],
|
||||
base: '/farm/',
|
||||
plugins: [vue(), redirectPlugin()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
@@ -23,6 +37,11 @@ export default defineConfig(({ mode }) => {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api')
|
||||
}
|
||||
},
|
||||
// 开发环境重定向配置
|
||||
middlewareMode: false,
|
||||
fs: {
|
||||
strict: false
|
||||
}
|
||||
},
|
||||
build: {
|
||||
@@ -38,7 +57,11 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
define: {
|
||||
// 将环境变量注入到应用中
|
||||
__APP_ENV__: JSON.stringify(env)
|
||||
__APP_ENV__: JSON.stringify(env),
|
||||
// 在生产环境中强制使用正确的API URL
|
||||
'import.meta.env.VITE_API_BASE_URL': JSON.stringify(
|
||||
mode === 'production' ? 'https://ad.ningmuyun.com/farm/api' : (env.VITE_API_BASE_URL || '/api')
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -7,7 +7,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const { verifyToken, checkRole } = require('../middleware/auth');
|
||||
const { requirePermission } = require('../middleware/permission');
|
||||
const { IotXqClient, IotJbqServer, IotJbqClient } = require('../models');
|
||||
const { IotXqClient, IotJbqServer, IotJbqClient, Farm } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const { createSuccessResponse, createErrorResponse, createPaginatedResponse, SUCCESS_MESSAGES, ERROR_CODES } = require('../utils/apiResponse');
|
||||
|
||||
@@ -1366,6 +1366,14 @@ router.get('/hosts', verifyToken, requirePermission('smart_host:view'), async (r
|
||||
]
|
||||
});
|
||||
|
||||
// 批量查询牧场信息,构建映射表
|
||||
const orgIds = [...new Set(rows.map(h => h.org_id).filter(id => id !== null && id !== undefined))];
|
||||
let farmMap = {};
|
||||
if (orgIds.length > 0) {
|
||||
const farms = await Farm.findAll({ where: { id: orgIds } });
|
||||
farmMap = Object.fromEntries(farms.map(f => [f.id, f.name]));
|
||||
}
|
||||
|
||||
// 格式化数据以匹配前端UI需求
|
||||
const hosts = rows.map(host => ({
|
||||
id: host.id,
|
||||
@@ -1383,6 +1391,9 @@ router.get('/hosts', verifyToken, requirePermission('smart_host:view'), async (r
|
||||
state: host.state, // 设备状态
|
||||
title: host.title, // 设备标题
|
||||
org_id: host.org_id, // 组织ID
|
||||
farmId: host.org_id, // 牧场ID(与组织ID一致)
|
||||
farmName: farmMap[host.org_id] || '-', // 牧场名称
|
||||
farm: { id: host.org_id, name: farmMap[host.org_id] || '-' },
|
||||
uid: host.uid, // 用户ID
|
||||
fence_id: host.fence_id, // 围栏ID
|
||||
source_id: host.source_id, // 数据源ID
|
||||
@@ -1449,6 +1460,19 @@ router.get('/hosts/:id', verifyToken, requirePermission('smart_host:view'), asyn
|
||||
}
|
||||
|
||||
// 格式化数据
|
||||
// 牧场信息
|
||||
let farmId = host.org_id;
|
||||
let farmName = '-';
|
||||
if (farmId !== null && farmId !== undefined) {
|
||||
try {
|
||||
const farm = await Farm.findByPk(farmId);
|
||||
if (farm && farm.name) {
|
||||
farmName = farm.name;
|
||||
}
|
||||
} catch (e) {
|
||||
// 牧场查询失败时忽略,不影响主机详情返回
|
||||
}
|
||||
}
|
||||
const hostData = {
|
||||
id: host.id,
|
||||
deviceNumber: host.sid,
|
||||
@@ -1465,6 +1489,9 @@ router.get('/hosts/:id', verifyToken, requirePermission('smart_host:view'), asyn
|
||||
state: host.state,
|
||||
title: host.title,
|
||||
org_id: host.org_id,
|
||||
farmId: farmId,
|
||||
farmName: farmName,
|
||||
farm: { id: farmId, name: farmName },
|
||||
uid: host.uid,
|
||||
fence_id: host.fence_id,
|
||||
source_id: host.source_id,
|
||||
|
||||
@@ -116,8 +116,28 @@ const handleResponse = async (response) => {
|
||||
const fetchRequest = async (url, options = {}) => {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 处理查询参数
|
||||
let finalUrl = url.startsWith('http') ? url : `${API_CONFIG.baseURL}${url}`
|
||||
|
||||
if (options.params) {
|
||||
const searchParams = new URLSearchParams()
|
||||
Object.keys(options.params).forEach(key => {
|
||||
if (options.params[key] !== undefined && options.params[key] !== null) {
|
||||
searchParams.append(key, options.params[key])
|
||||
}
|
||||
})
|
||||
|
||||
const queryString = searchParams.toString()
|
||||
if (queryString) {
|
||||
finalUrl += (finalUrl.includes('?') ? '&' : '?') + queryString
|
||||
}
|
||||
|
||||
// 从options中移除params,避免传递给fetch
|
||||
delete options.params
|
||||
}
|
||||
|
||||
// 构建完整URL
|
||||
const fullUrl = url.startsWith('http') ? url : `${API_CONFIG.baseURL}${url}`
|
||||
const fullUrl = finalUrl
|
||||
|
||||
// 对于登录、刷新token接口,跳过token检查
|
||||
const skipTokenCheck = url.includes('/auth/login') ||
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
>
|
||||
<a-form-item label="申请单号">
|
||||
<a-input
|
||||
v-model:value="searchForm.applicationNumber"
|
||||
v-model:value="searchForm.application_no"
|
||||
placeholder="请输入申请单号"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="投保人姓名">
|
||||
<a-input
|
||||
v-model:value="searchForm.applicantName"
|
||||
v-model:value="searchForm.customer_name"
|
||||
placeholder="请输入投保人姓名"
|
||||
allow-clear
|
||||
/>
|
||||
@@ -170,31 +170,31 @@
|
||||
size="small"
|
||||
>
|
||||
<a-descriptions-item label="申请单号" :span="2">
|
||||
{{ selectedApplication.application_number }}
|
||||
{{ selectedApplication.application_no }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="参保类型">
|
||||
{{ selectedApplication.insurance_category === 'individual' ? '个人参保' : '企业参保' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="参保险种">
|
||||
{{ selectedApplication.insurance_type }}
|
||||
{{ selectedApplication.insurance_type?.name || selectedApplication.insurance_type_id }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="保险金额">
|
||||
¥{{ Number(selectedApplication.insurance_amount).toLocaleString() }}
|
||||
¥{{ Number(selectedApplication.application_amount).toLocaleString() }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="保险期限">
|
||||
{{ selectedApplication.insurance_period }}个月
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="投保人姓名">
|
||||
{{ selectedApplication.applicant_name }}
|
||||
{{ selectedApplication.customer_name }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="身份证号">
|
||||
{{ maskIdCard(selectedApplication.id_card) }}
|
||||
{{ maskIdCard(selectedApplication.customer_id_card) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">
|
||||
{{ maskPhone(selectedApplication.phone) }}
|
||||
{{ maskPhone(selectedApplication.customer_phone) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="联系地址" :span="2">
|
||||
{{ selectedApplication.address }}
|
||||
{{ selectedApplication.customer_address }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="申请时间">
|
||||
{{ selectedApplication.application_date }}
|
||||
@@ -277,20 +277,20 @@
|
||||
<a-radio value="enterprise">企业参保</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="参保险种" name="insurance_type" :rules="[{ required: true, message: '请选择参保险种' }]">
|
||||
<a-select v-model:value="createForm.insurance_type" placeholder="请选择参保险种">
|
||||
<a-form-item label="参保险种" name="insurance_type_id" :rules="[{ required: true, message: '请选择参保险种' }]">
|
||||
<a-select v-model:value="createForm.insurance_type_id" placeholder="请选择参保险种">
|
||||
<a-select-option
|
||||
v-for="type in insuranceTypes"
|
||||
:key="type.id"
|
||||
:value="type.name"
|
||||
:value="type.id"
|
||||
>
|
||||
{{ type.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="保险金额" name="insurance_amount" :rules="[{ required: true, message: '请输入保险金额' }]">
|
||||
<a-form-item label="保险金额" name="application_amount" :rules="[{ required: true, message: '请输入保险金额' }]">
|
||||
<a-input-number
|
||||
v-model:value="createForm.insurance_amount"
|
||||
v-model:value="createForm.application_amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
@@ -304,17 +304,17 @@
|
||||
<a-select-option value="36">36个月</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="投保人姓名" name="applicant_name" :rules="[{ required: true, message: '请输入投保人姓名' }]">
|
||||
<a-input v-model:value="createForm.applicant_name" placeholder="请输入投保人姓名" />
|
||||
<a-form-item label="投保人姓名" name="customer_name" :rules="[{ required: true, message: '请输入投保人姓名' }]">
|
||||
<a-input v-model:value="createForm.customer_name" placeholder="请输入投保人姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="身份证号" name="id_card" :rules="[{ required: true, message: '请输入身份证号' }]">
|
||||
<a-input v-model:value="createForm.id_card" placeholder="请输入身份证号" />
|
||||
<a-form-item label="身份证号" name="customer_id_card" :rules="[{ required: true, message: '请输入身份证号' }]">
|
||||
<a-input v-model:value="createForm.customer_id_card" placeholder="请输入身份证号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系电话" name="phone" :rules="[{ required: true, message: '请输入联系电话' }]">
|
||||
<a-input v-model:value="createForm.phone" placeholder="请输入联系电话" />
|
||||
<a-form-item label="联系电话" name="customer_phone" :rules="[{ required: true, message: '请输入联系电话' }]">
|
||||
<a-input v-model:value="createForm.customer_phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系地址" name="address" :rules="[{ required: true, message: '请输入联系地址' }]">
|
||||
<a-textarea v-model:value="createForm.address" placeholder="请输入联系地址" />
|
||||
<a-form-item label="联系地址" name="customer_address" :rules="[{ required: true, message: '请输入联系地址' }]">
|
||||
<a-textarea v-model:value="createForm.customer_address" placeholder="请输入联系地址" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remarks">
|
||||
<a-textarea v-model:value="createForm.remarks" placeholder="请输入备注信息" />
|
||||
@@ -347,8 +347,8 @@ const selectedApplication = ref(null)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
applicationNumber: '',
|
||||
applicantName: '',
|
||||
application_no: '',
|
||||
customer_name: '',
|
||||
insuranceType: '',
|
||||
insuranceCategory: '',
|
||||
status: ''
|
||||
@@ -366,13 +366,13 @@ const reviewFormData = reactive({
|
||||
// 新增申请表单
|
||||
const createForm = reactive({
|
||||
insurance_category: '',
|
||||
insurance_type: '',
|
||||
insurance_amount: null,
|
||||
insurance_type_id: '',
|
||||
application_amount: null,
|
||||
insurance_period: '',
|
||||
applicant_name: '',
|
||||
id_card: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
customer_name: '',
|
||||
customer_id_card: '',
|
||||
customer_phone: '',
|
||||
customer_address: '',
|
||||
remarks: ''
|
||||
})
|
||||
|
||||
@@ -390,14 +390,14 @@ const pagination = reactive({
|
||||
const columns = [
|
||||
{
|
||||
title: '申请单号',
|
||||
dataIndex: 'application_number',
|
||||
key: 'application_number',
|
||||
dataIndex: 'application_no',
|
||||
key: 'application_no',
|
||||
width: 180,
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '投保人姓名',
|
||||
dataIndex: 'applicant_name',
|
||||
dataIndex: 'customer_name',
|
||||
key: 'applicant_name',
|
||||
width: 120
|
||||
},
|
||||
@@ -409,14 +409,17 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: '参保险种',
|
||||
dataIndex: 'insurance_type',
|
||||
key: 'insurance_type',
|
||||
width: 150
|
||||
dataIndex: 'insurance_type_id',
|
||||
key: 'insurance_type_id',
|
||||
width: 150,
|
||||
customRender: ({ record }) => {
|
||||
return record.insurance_type?.name || record.insurance_type_id
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '保险金额',
|
||||
dataIndex: 'insurance_amount',
|
||||
key: 'insurance_amount',
|
||||
dataIndex: 'application_amount',
|
||||
key: 'application_amount',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
@@ -427,8 +430,8 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'phone',
|
||||
key: 'phone',
|
||||
dataIndex: 'customer_phone',
|
||||
key: 'customer_phone',
|
||||
width: 130
|
||||
},
|
||||
{
|
||||
@@ -517,8 +520,8 @@ const handleSearch = () => {
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
applicationNumber: '',
|
||||
applicantName: '',
|
||||
application_no: '',
|
||||
customer_name: '',
|
||||
insuranceType: '',
|
||||
insuranceCategory: '',
|
||||
status: ''
|
||||
@@ -575,13 +578,13 @@ const showCreateModal = () => {
|
||||
isEdit.value = false
|
||||
Object.assign(createForm, {
|
||||
insurance_category: '',
|
||||
insurance_type: '',
|
||||
insurance_amount: null,
|
||||
insurance_type_id: '',
|
||||
application_amount: null,
|
||||
insurance_period: '',
|
||||
applicant_name: '',
|
||||
id_card: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
customer_name: '',
|
||||
customer_id_card: '',
|
||||
customer_phone: '',
|
||||
customer_address: '',
|
||||
remarks: ''
|
||||
})
|
||||
createModalVisible.value = true
|
||||
|
||||
@@ -17,37 +17,74 @@
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="理赔单号">
|
||||
<a-input
|
||||
v-model:value="searchForm.claim_number"
|
||||
v-model:value="searchForm.claim_no"
|
||||
placeholder="请输入理赔单号"
|
||||
@pressEnter="handleSearch"
|
||||
@change="onSearchFieldChange('claim_no', $event)"
|
||||
@input="onSearchFieldInput('claim_no', $event)"
|
||||
allowClear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="保单号">
|
||||
<a-input
|
||||
v-model:value="searchForm.policy_number"
|
||||
v-model:value="searchForm.policy_no"
|
||||
placeholder="请输入保单号"
|
||||
@pressEnter="handleSearch"
|
||||
@change="onSearchFieldChange('policy_no', $event)"
|
||||
@input="onSearchFieldInput('policy_no', $event)"
|
||||
allowClear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="申请人">
|
||||
<a-form-item label="报案人">
|
||||
<a-input
|
||||
v-model:value="searchForm.applicant_name"
|
||||
placeholder="请输入申请人姓名"
|
||||
v-model:value="searchForm.reporter_name"
|
||||
placeholder="请输入报案人姓名"
|
||||
@pressEnter="handleSearch"
|
||||
@change="onSearchFieldChange('reporter_name', $event)"
|
||||
@input="onSearchFieldInput('reporter_name', $event)"
|
||||
allowClear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="联系电话">
|
||||
<a-input
|
||||
v-model:value="searchForm.contact_phone"
|
||||
placeholder="请输入联系电话"
|
||||
@pressEnter="handleSearch"
|
||||
@change="onSearchFieldChange('contact_phone', $event)"
|
||||
@input="onSearchFieldInput('contact_phone', $event)"
|
||||
allowClear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="事故类型">
|
||||
<a-select
|
||||
v-model:value="searchForm.claim_type"
|
||||
placeholder="请选择事故类型"
|
||||
style="width: 120px"
|
||||
@change="onSearchFieldChange('claim_type', $event)"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="disease">疾病</a-select-option>
|
||||
<a-select-option value="accident">意外事故</a-select-option>
|
||||
<a-select-option value="natural_disaster">自然灾害</a-select-option>
|
||||
<a-select-option value="theft">盗窃</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.status"
|
||||
v-model:value="searchForm.claim_status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 120px"
|
||||
@change="onSearchFieldChange('claim_status', $event)"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
<a-select-option value="investigating">调查中</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="processing">处理中</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="paid">已赔付</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
@@ -73,41 +110,62 @@
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
<template v-if="column.key === 'claim_status'">
|
||||
<a-tag :color="getStatusColor(record.claim_status)">
|
||||
{{ getStatusText(record.claim_status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'claim_type'">
|
||||
<a-tag :color="getClaimTypeColor(record.claim_type)">
|
||||
{{ getClaimTypeText(record.claim_type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'claim_amount'">
|
||||
<span>¥{{ record.claim_amount?.toLocaleString() }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'approved_amount'">
|
||||
<span v-if="record.approved_amount">¥{{ record.approved_amount?.toLocaleString() }}</span>
|
||||
<span v-else>-</span>
|
||||
<template v-else-if="column.key === 'contact_phone'">
|
||||
<span>{{ record.contact_phone || '-' }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="handleView(record)">查看</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:type="record.status === 'pending' ? 'primary' : 'default'"
|
||||
@click="handleProcess(record)"
|
||||
:disabled="record.status !== 'pending'"
|
||||
type="primary"
|
||||
@click="handleEdit(record)"
|
||||
>
|
||||
处理
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这条理赔记录吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record.id)"
|
||||
>
|
||||
<a-button size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
<a-dropdown>
|
||||
<a-button size="small">更多</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="handleEdit(record)">编辑</a-menu-item>
|
||||
<a-menu-item @click="handleApprove(record)" :disabled="record.status !== 'pending'">
|
||||
<a-menu-item
|
||||
@click="handleProcess(record)"
|
||||
:disabled="record.claim_status !== 'pending'"
|
||||
>
|
||||
处理
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
@click="handleApprove(record)"
|
||||
:disabled="record.claim_status !== 'pending'"
|
||||
>
|
||||
通过
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="handleReject(record)" :disabled="record.status !== 'pending'">
|
||||
<a-menu-item
|
||||
@click="handleReject(record)"
|
||||
:disabled="record.claim_status !== 'pending'"
|
||||
>
|
||||
拒绝
|
||||
</a-menu-item>
|
||||
<a-menu-item danger @click="handleDelete(record.id)">删除</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
@@ -130,26 +188,29 @@
|
||||
bordered
|
||||
:column="2"
|
||||
>
|
||||
<a-descriptions-item label="理赔单号">{{ currentClaim.claim_number }}</a-descriptions-item>
|
||||
<a-descriptions-item label="保单号">{{ currentClaim.policy_number }}</a-descriptions-item>
|
||||
<a-descriptions-item label="申请人">{{ currentClaim.applicant_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">{{ currentClaim.phone }}</a-descriptions-item>
|
||||
<a-descriptions-item label="理赔单号">{{ currentClaim.claim_no }}</a-descriptions-item>
|
||||
<a-descriptions-item label="报案人">{{ currentClaim.reporter_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">{{ currentClaim.contact_phone }}</a-descriptions-item>
|
||||
<a-descriptions-item label="保单号">{{ currentClaim.policy_no }}</a-descriptions-item>
|
||||
<a-descriptions-item label="事故类型">{{ currentClaim.claim_type }}</a-descriptions-item>
|
||||
<a-descriptions-item label="受影响数量">{{ currentClaim.affected_count }}</a-descriptions-item>
|
||||
<a-descriptions-item label="申请金额">¥{{ currentClaim.claim_amount?.toLocaleString() }}</a-descriptions-item>
|
||||
<a-descriptions-item label="审核金额" v-if="currentClaim.approved_amount">
|
||||
¥{{ currentClaim.approved_amount?.toLocaleString() }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="申请时间">{{ currentClaim.apply_date }}</a-descriptions-item>
|
||||
<a-descriptions-item label="事故时间">{{ currentClaim.accident_date }}</a-descriptions-item>
|
||||
<a-descriptions-item label="出险地址">{{ currentClaim.incident_location }}</a-descriptions-item>
|
||||
<a-descriptions-item label="报案时间">{{ currentClaim.report_date }}</a-descriptions-item>
|
||||
<a-descriptions-item label="事故时间">{{ currentClaim.incident_date }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(currentClaim.status)">
|
||||
{{ getStatusText(currentClaim.status) }}
|
||||
<a-tag :color="getStatusColor(currentClaim.claim_status)">
|
||||
{{ getStatusText(currentClaim.claim_status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="审核人">{{ currentClaim.reviewer_name || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="调查员">{{ currentClaim.investigator_name || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="调查时间">{{ currentClaim.investigation_date || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="审核员">{{ currentClaim.reviewer_name || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="审核时间">{{ currentClaim.review_date || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="拒绝原因" :span="2" v-if="currentClaim.reject_reason">
|
||||
{{ currentClaim.reject_reason }}
|
||||
<a-descriptions-item label="赔付金额" v-if="currentClaim.payment_amount">
|
||||
¥{{ currentClaim.payment_amount?.toLocaleString() }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="赔付时间">{{ currentClaim.payment_date || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-divider />
|
||||
@@ -259,26 +320,50 @@
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="理赔单号" name="claim_number">
|
||||
<a-input v-model:value="formState.claim_number" placeholder="请输入理赔单号" />
|
||||
<a-form-item label="理赔单号" name="claim_no">
|
||||
<a-input v-model:value="formState.claim_no" placeholder="请输入理赔单号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="保单号" name="policy_number">
|
||||
<a-input v-model:value="formState.policy_number" placeholder="请输入保单号" />
|
||||
<a-form-item label="保单号" name="policy_no">
|
||||
<a-input v-model:value="formState.policy_no" placeholder="请输入保单号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="申请人姓名" name="applicant_name">
|
||||
<a-input v-model:value="formState.applicant_name" placeholder="请输入申请人姓名" />
|
||||
<a-form-item label="报案人" name="reporter_name">
|
||||
<a-input v-model:value="formState.reporter_name" placeholder="请输入报案人姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input v-model:value="formState.phone" placeholder="请输入联系电话" />
|
||||
<a-form-item label="联系电话" name="contact_phone">
|
||||
<a-input v-model:value="formState.contact_phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="事故类型" name="claim_type">
|
||||
<a-select v-model:value="formState.claim_type" placeholder="请选择事故类型">
|
||||
<a-select-option value="disease">疾病</a-select-option>
|
||||
<a-select-option value="accident">意外</a-select-option>
|
||||
<a-select-option value="natural_disaster">自然灾害</a-select-option>
|
||||
<a-select-option value="other">其他</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="受影响数量" name="affected_count">
|
||||
<a-input-number
|
||||
v-model:value="formState.affected_count"
|
||||
:min="0"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
placeholder="请输入受影响数量"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -296,34 +381,30 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="申请日期" name="apply_date">
|
||||
<a-date-picker
|
||||
v-model:value="formState.apply_date"
|
||||
style="width: 100%"
|
||||
placeholder="请选择申请日期"
|
||||
/>
|
||||
<a-form-item label="出险地址" name="incident_location">
|
||||
<a-input v-model:value="formState.incident_location" placeholder="请输入出险地址" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="事故日期" name="accident_date">
|
||||
<a-form-item label="事故日期" name="incident_date">
|
||||
<a-date-picker
|
||||
v-model:value="formState.accident_date"
|
||||
v-model:value="formState.incident_date"
|
||||
style="width: 100%"
|
||||
placeholder="请选择事故日期"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status" placeholder="请选择状态">
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="processing">处理中</a-select-option>
|
||||
<a-form-item label="状态" name="claim_status">
|
||||
<a-select v-model:value="formState.claim_status" placeholder="请选择状态">
|
||||
<a-select-option value="pending">待处理</a-select-option>
|
||||
<a-select-option value="investigating">调查中</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
<a-select-option value="completed">已完成</a-select-option>
|
||||
<a-select-option value="paid">已赔付</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -366,7 +447,8 @@ import {
|
||||
RedoOutlined,
|
||||
FileTextOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { claimAPI } from '@/utils/api'
|
||||
import { livestockClaimApi } from '@/utils/api'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
@@ -379,21 +461,25 @@ const formRef = ref()
|
||||
const processFormRef = ref()
|
||||
|
||||
const searchForm = reactive({
|
||||
claim_number: '',
|
||||
policy_number: '',
|
||||
applicant_name: '',
|
||||
status: ''
|
||||
claim_no: '',
|
||||
policy_no: '',
|
||||
reporter_name: '',
|
||||
contact_phone: '',
|
||||
claim_type: '',
|
||||
claim_status: ''
|
||||
})
|
||||
|
||||
const formState = reactive({
|
||||
claim_number: '',
|
||||
policy_number: '',
|
||||
applicant_name: '',
|
||||
phone: '',
|
||||
claim_no: '',
|
||||
policy_no: '',
|
||||
reporter_name: '',
|
||||
contact_phone: '',
|
||||
claim_type: '',
|
||||
affected_count: null,
|
||||
claim_amount: null,
|
||||
apply_date: null,
|
||||
accident_date: null,
|
||||
status: 'pending',
|
||||
incident_location: '',
|
||||
incident_date: null,
|
||||
claim_status: 'pending',
|
||||
accident_description: '',
|
||||
process_description: '',
|
||||
reject_reason: '',
|
||||
@@ -419,45 +505,63 @@ const pagination = reactive({
|
||||
const columns = [
|
||||
{
|
||||
title: '理赔单号',
|
||||
dataIndex: 'claim_number',
|
||||
key: 'claim_number'
|
||||
dataIndex: 'claim_no',
|
||||
key: 'claim_no',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '报案人',
|
||||
dataIndex: 'reporter_name',
|
||||
key: 'reporter_name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'contact_phone',
|
||||
key: 'contact_phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '保单号',
|
||||
dataIndex: 'policy_number',
|
||||
key: 'policy_number'
|
||||
dataIndex: 'policy_no',
|
||||
key: 'policy_no',
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
title: '申请人',
|
||||
dataIndex: 'applicant_name',
|
||||
key: 'applicant_name'
|
||||
title: '事故类型',
|
||||
dataIndex: 'claim_type',
|
||||
key: 'claim_type',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '申请数量',
|
||||
dataIndex: 'affected_count',
|
||||
key: 'affected_count',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '申请金额',
|
||||
key: 'claim_amount',
|
||||
dataIndex: 'claim_amount'
|
||||
},
|
||||
{
|
||||
title: '审核金额',
|
||||
key: 'approved_amount',
|
||||
dataIndex: 'approved_amount'
|
||||
},
|
||||
{
|
||||
title: '申请日期',
|
||||
dataIndex: 'apply_date',
|
||||
key: 'apply_date',
|
||||
dataIndex: 'claim_amount',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
dataIndex: 'status'
|
||||
title: '出险地址',
|
||||
dataIndex: 'incident_location',
|
||||
key: 'incident_location',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180
|
||||
title: '状态',
|
||||
key: 'claim_status',
|
||||
dataIndex: 'claim_status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '报案时间',
|
||||
dataIndex: 'report_date',
|
||||
key: 'report_date',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -468,13 +572,16 @@ const columns = [
|
||||
]
|
||||
|
||||
const rules = {
|
||||
claim_number: [{ required: true, message: '请输入理赔单号' }],
|
||||
policy_number: [{ required: true, message: '请输入保单号' }],
|
||||
applicant_name: [{ required: true, message: '请输入申请人姓名' }],
|
||||
claim_no: [{ required: true, message: '请输入理赔单号' }],
|
||||
policy_no: [{ required: true, message: '请输入保单号' }],
|
||||
reporter_name: [{ required: true, message: '请输入报案人姓名' }],
|
||||
contact_phone: [{ required: true, message: '请输入联系电话' }],
|
||||
claim_type: [{ required: true, message: '请选择事故类型' }],
|
||||
affected_count: [{ required: true, message: '请输入受影响数量' }],
|
||||
claim_amount: [{ required: true, message: '请输入申请金额' }],
|
||||
apply_date: [{ required: true, message: '请选择申请日期' }],
|
||||
accident_date: [{ required: true, message: '请选择事故日期' }],
|
||||
status: [{ required: true, message: '请选择状态' }],
|
||||
incident_location: [{ required: true, message: '请输入出险地址' }],
|
||||
incident_date: [{ required: true, message: '请选择事故日期' }],
|
||||
claim_status: [{ required: true, message: '请选择状态' }],
|
||||
accident_description: [{ required: true, message: '请输入事故描述' }]
|
||||
}
|
||||
|
||||
@@ -522,47 +629,192 @@ const getStatusText = (status) => {
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
const getClaimTypeText = (type) => {
|
||||
const typeMap = {
|
||||
disease: '疾病',
|
||||
accident: '意外事故',
|
||||
natural_disaster: '自然灾害',
|
||||
theft: '盗窃',
|
||||
other: '其他'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
const getClaimTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
disease: 'red',
|
||||
accident: 'orange',
|
||||
natural_disaster: 'purple',
|
||||
theft: 'volcano',
|
||||
other: 'default'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
const loadClaims = async () => {
|
||||
loading.value = true
|
||||
console.log('🚀 开始加载理赔数据...')
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...searchForm
|
||||
pageSize: pagination.pageSize
|
||||
}
|
||||
|
||||
console.log('理赔管理API请求参数:', params)
|
||||
const response = await claimAPI.getList(params)
|
||||
console.log('理赔管理API响应:', response)
|
||||
console.log('📋 当前搜索表单状态:', JSON.stringify(searchForm, null, 2))
|
||||
|
||||
// 添加搜索条件
|
||||
if (searchForm.claim_no) {
|
||||
params.claim_no = searchForm.claim_no
|
||||
console.log('🔍 添加理赔单号搜索条件:', searchForm.claim_no)
|
||||
}
|
||||
if (searchForm.policy_no) {
|
||||
params.policy_no = searchForm.policy_no
|
||||
console.log('🔍 添加保单号搜索条件:', searchForm.policy_no)
|
||||
}
|
||||
if (searchForm.reporter_name) {
|
||||
params.reporter_name = searchForm.reporter_name
|
||||
console.log('🔍 添加报案人搜索条件:', searchForm.reporter_name)
|
||||
}
|
||||
if (searchForm.contact_phone) {
|
||||
params.contact_phone = searchForm.contact_phone
|
||||
console.log('🔍 添加联系电话搜索条件:', searchForm.contact_phone)
|
||||
}
|
||||
if (searchForm.claim_type) {
|
||||
params.claim_type = searchForm.claim_type
|
||||
console.log('🔍 添加事故类型搜索条件:', searchForm.claim_type)
|
||||
}
|
||||
if (searchForm.claim_status) {
|
||||
params.claim_status = searchForm.claim_status
|
||||
console.log('🔍 添加状态搜索条件:', searchForm.claim_status)
|
||||
}
|
||||
|
||||
console.log('📤 [前端->后端] API请求参数:', {
|
||||
请求时间: new Date().toLocaleString(),
|
||||
请求URL: '/api/livestock-claims',
|
||||
请求方法: 'GET',
|
||||
请求参数: JSON.stringify(params, null, 2),
|
||||
分页信息: {
|
||||
当前页: pagination.current,
|
||||
每页条数: pagination.pageSize
|
||||
}
|
||||
})
|
||||
|
||||
const response = await livestockClaimApi.getList(params)
|
||||
|
||||
console.log('📥 [后端->前端] API响应:', {
|
||||
响应时间: new Date().toLocaleString(),
|
||||
响应状态: response.status,
|
||||
响应头: response.headers,
|
||||
响应数据结构: {
|
||||
status: response.data?.status,
|
||||
message: response.data?.message,
|
||||
data类型: Array.isArray(response.data?.data) ? 'Array' : typeof response.data?.data,
|
||||
data长度: response.data?.data?.length,
|
||||
pagination: response.data?.pagination
|
||||
},
|
||||
完整响应: JSON.stringify(response.data, null, 2)
|
||||
})
|
||||
|
||||
if (response.data && response.data.status === 'success') {
|
||||
// 后端返回的数据直接是数组格式,不是{list: [], total: 8}格式
|
||||
claimList.value = response.data.data || []
|
||||
pagination.total = response.data.pagination?.total || 0
|
||||
console.log('理赔管理数据设置成功:', claimList.value.length, '条')
|
||||
const newClaimList = response.data.data || []
|
||||
const newTotal = response.data.pagination?.total || 0
|
||||
|
||||
console.log('✅ 数据处理成功:', {
|
||||
处理时间: new Date().toLocaleString(),
|
||||
数据条数: newClaimList.length,
|
||||
总记录数: newTotal,
|
||||
数据样例: newClaimList.length > 0 ? JSON.stringify(newClaimList[0], null, 2) : '无数据'
|
||||
})
|
||||
|
||||
claimList.value = newClaimList
|
||||
pagination.total = newTotal
|
||||
|
||||
console.log('📊 前端状态更新:', {
|
||||
更新时间: new Date().toLocaleString(),
|
||||
列表长度: claimList.value.length,
|
||||
分页总数: pagination.total,
|
||||
当前页: pagination.current,
|
||||
每页条数: pagination.pageSize
|
||||
})
|
||||
} else {
|
||||
console.log('理赔管理响应格式错误:', response)
|
||||
console.error('❌ 响应格式错误:', {
|
||||
错误时间: new Date().toLocaleString(),
|
||||
响应状态: response.data?.status,
|
||||
错误信息: response.data?.message,
|
||||
完整响应: response
|
||||
})
|
||||
message.error(response.data?.message || '加载理赔列表失败')
|
||||
claimList.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('加载理赔列表失败')
|
||||
console.error('💥 API请求失败:', {
|
||||
错误时间: new Date().toLocaleString(),
|
||||
错误类型: error.name,
|
||||
错误信息: error.message,
|
||||
错误堆栈: error.stack,
|
||||
请求配置: error.config,
|
||||
响应数据: error.response?.data,
|
||||
响应状态: error.response?.status
|
||||
})
|
||||
message.error('加载理赔列表失败: ' + error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
console.log('🏁 理赔数据加载完成')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
console.log('🔍 执行搜索操作:', {
|
||||
触发时间: new Date().toLocaleString(),
|
||||
搜索条件: JSON.stringify(searchForm, null, 2),
|
||||
重置分页: '第1页'
|
||||
})
|
||||
pagination.current = 1
|
||||
loadClaims()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
console.log('🔄 重置搜索表单')
|
||||
searchForm.claim_no = ''
|
||||
searchForm.policy_no = ''
|
||||
searchForm.reporter_name = ''
|
||||
searchForm.contact_phone = ''
|
||||
searchForm.claim_type = ''
|
||||
searchForm.claim_status = ''
|
||||
console.log('🔄 重置后的搜索表单:', JSON.stringify(searchForm, null, 2))
|
||||
loadClaims()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
searchForm.claim_number = ''
|
||||
searchForm.policy_number = ''
|
||||
searchForm.applicant_name = ''
|
||||
searchForm.status = ''
|
||||
handleSearch()
|
||||
// 搜索字段输入监听器
|
||||
const onSearchFieldInput = (fieldName, event) => {
|
||||
const value = event.target ? event.target.value : event
|
||||
console.log(`📝 [输入监听] ${fieldName}:`, {
|
||||
输入值: value,
|
||||
事件类型: 'input',
|
||||
时间戳: new Date().toLocaleString(),
|
||||
当前表单状态: JSON.stringify(searchForm, null, 2)
|
||||
})
|
||||
}
|
||||
|
||||
// 搜索字段变化监听器
|
||||
const onSearchFieldChange = (fieldName, event) => {
|
||||
const value = event.target ? event.target.value : event
|
||||
console.log(`🔍 [变化监听] ${fieldName}:`, {
|
||||
变化值: value,
|
||||
事件类型: 'change',
|
||||
时间戳: new Date().toLocaleString(),
|
||||
变化前表单: JSON.stringify(searchForm, null, 2)
|
||||
})
|
||||
|
||||
// 延迟一点时间确保 v-model 已更新
|
||||
setTimeout(() => {
|
||||
console.log(`🔍 [变化后] ${fieldName}:`, {
|
||||
表单中的值: searchForm[fieldName],
|
||||
变化后表单: JSON.stringify(searchForm, null, 2)
|
||||
})
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
@@ -574,14 +826,16 @@ const handleTableChange = (pag) => {
|
||||
const showModal = () => {
|
||||
editingId.value = null
|
||||
Object.assign(formState, {
|
||||
claim_number: '',
|
||||
policy_number: '',
|
||||
applicant_name: '',
|
||||
phone: '',
|
||||
claim_no: '',
|
||||
policy_no: '',
|
||||
reporter_name: '',
|
||||
contact_phone: '',
|
||||
claim_type: '',
|
||||
affected_count: null,
|
||||
claim_amount: null,
|
||||
apply_date: null,
|
||||
accident_date: null,
|
||||
status: 'pending',
|
||||
incident_location: '',
|
||||
incident_date: null,
|
||||
claim_status: 'pending',
|
||||
accident_description: '',
|
||||
process_description: '',
|
||||
reject_reason: '',
|
||||
@@ -590,6 +844,10 @@ const showModal = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
showModal()
|
||||
}
|
||||
|
||||
const handleView = (record) => {
|
||||
currentClaim.value = record
|
||||
detailVisible.value = true
|
||||
@@ -608,26 +866,31 @@ const handleProcess = (record) => {
|
||||
|
||||
const handleEdit = (record) => {
|
||||
editingId.value = record.id
|
||||
|
||||
// 复制记录数据到表单
|
||||
Object.assign(formState, {
|
||||
claim_number: record.claim_number,
|
||||
policy_number: record.policy_number,
|
||||
applicant_name: record.applicant_name,
|
||||
phone: record.phone,
|
||||
claim_no: record.claim_no,
|
||||
policy_no: record.policy_no,
|
||||
reporter_name: record.reporter_name,
|
||||
contact_phone: record.contact_phone,
|
||||
claim_type: record.claim_type,
|
||||
affected_count: record.affected_count,
|
||||
claim_amount: record.claim_amount,
|
||||
apply_date: record.apply_date,
|
||||
accident_date: record.accident_date,
|
||||
status: record.status,
|
||||
incident_location: record.incident_location,
|
||||
incident_date: record.incident_date ? dayjs(record.incident_date) : null,
|
||||
claim_status: record.claim_status,
|
||||
accident_description: record.accident_description,
|
||||
process_description: record.process_description,
|
||||
reject_reason: record.reject_reason,
|
||||
approved_amount: record.approved_amount
|
||||
})
|
||||
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleApprove = async (record) => {
|
||||
try {
|
||||
// await claimAPI.approve(record.id, { approved_amount: record.claim_amount })
|
||||
// await livestockClaimApi.approve(record.id, { approved_amount: record.claim_amount })
|
||||
message.success('理赔申请已通过')
|
||||
loadClaims()
|
||||
} catch (error) {
|
||||
@@ -643,7 +906,7 @@ const handleProcessOk = async () => {
|
||||
try {
|
||||
await processFormRef.value.validate()
|
||||
|
||||
// await claimAPI.process(currentClaim.value.id, processForm)
|
||||
// await livestockClaimApi.process(currentClaim.value.id, processForm)
|
||||
message.success('处理完成')
|
||||
processVisible.value = false
|
||||
loadClaims()
|
||||
@@ -660,18 +923,32 @@ const handleModalOk = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
const formData = { ...formState }
|
||||
|
||||
// 处理日期格式
|
||||
if (formData.incident_date) {
|
||||
formData.incident_date = formData.incident_date.format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
if (editingId.value) {
|
||||
// await claimAPI.update(editingId.value, formState)
|
||||
message.success('理赔更新成功')
|
||||
// 编辑
|
||||
await livestockClaimApi.update(editingId.value, formData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
// await claimAPI.create(formState)
|
||||
message.success('理赔创建成功')
|
||||
// 新增
|
||||
// 生成理赔单号
|
||||
if (!formData.claim_no) {
|
||||
formData.claim_no = 'LC' + Date.now()
|
||||
}
|
||||
await livestockClaimApi.create(formData)
|
||||
message.success('新增成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
loadClaims()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败', error)
|
||||
console.error('保存失败:', error)
|
||||
message.error('保存失败: ' + (error.response?.data?.message || error.message))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,11 +958,12 @@ const handleModalCancel = () => {
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
// await claimAPI.delete(id)
|
||||
message.success('理赔删除成功')
|
||||
await livestockClaimApi.delete(id)
|
||||
message.success('删除成功')
|
||||
loadClaims()
|
||||
} catch (error) {
|
||||
message.error('理赔删除失败')
|
||||
console.error('删除失败:', error)
|
||||
message.error('删除失败: ' + (error.response?.data?.message || error.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,36 +17,12 @@
|
||||
<a-form layout="inline" :model="searchForm">
|
||||
<a-form-item label="险种名称">
|
||||
<a-input
|
||||
v-model:value="searchForm.name"
|
||||
v-model="searchForm.name"
|
||||
placeholder="请输入险种名称"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="适用范围">
|
||||
<a-input
|
||||
v-model:value="searchForm.applicable_scope"
|
||||
placeholder="请输入适用范围"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="服务区域">
|
||||
<a-input
|
||||
v-model:value="searchForm.service_area"
|
||||
placeholder="请输入服务区域"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="在线状态">
|
||||
<a-select
|
||||
v-model:value="searchForm.online_status"
|
||||
placeholder="请选择在线状态"
|
||||
style="width: 120px"
|
||||
>
|
||||
<a-select-option value="">全部</a-select-option>
|
||||
<a-select-option :value="true">在线</a-select-option>
|
||||
<a-select-option :value="false">离线</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<search-outlined />
|
||||
@@ -71,25 +47,25 @@
|
||||
:scroll="{ x: 1500 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'online_status'">
|
||||
<template v-if="column.key === 'on_sale_status'">
|
||||
<a-switch
|
||||
:checked="record.online_status"
|
||||
@change="(checked) => handleToggleOnlineStatus(record, checked)"
|
||||
checked-children="在线"
|
||||
un-checked-children="离线"
|
||||
:checked="record.on_sale_status"
|
||||
@change="(checked) => handleToggleOnSaleStatus(record, checked)"
|
||||
checked-children="在售"
|
||||
un-checked-children="停售"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'premium_price'">
|
||||
<span>{{ record.premium_price ? `¥${record.premium_price}` : '-' }}</span>
|
||||
<template v-else-if="column.key === 'applicable_livestock'">
|
||||
<span>{{ formatApplicableLivestock(record.applicable_livestock) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'experience_period'">
|
||||
<span>{{ record.experience_period ? `${record.experience_period}个月` : '-' }}</span>
|
||||
<template v-else-if="column.key === 'coverage_amount'">
|
||||
<span>{{ formatCoverageAmount(record.coverage_amount_min, record.coverage_amount_max) }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'insurance_period'">
|
||||
<span>{{ record.insurance_period ? `${record.insurance_period}个月` : '-' }}</span>
|
||||
<template v-else-if="column.key === 'insurance_term'">
|
||||
<span>{{ record.insurance_term ? `${record.insurance_term}个月` : '-' }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'product_time'">
|
||||
<span>{{ record.product_time ? formatDate(record.product_time) : '-' }}</span>
|
||||
<template v-else-if="column.key === 'created_at'">
|
||||
<span>{{ record.created_at ? formatDate(record.created_at) : '-' }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
@@ -109,7 +85,8 @@
|
||||
|
||||
<!-- 新增/编辑模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:open="modalVisible"
|
||||
@update:open="modalVisible = $event"
|
||||
:title="isEdit ? '编辑险种' : '新增险种'"
|
||||
:ok-text="isEdit ? '更新' : '创建'"
|
||||
cancel-text="取消"
|
||||
@@ -126,111 +103,163 @@
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="险种名称" name="name">
|
||||
<a-input v-model:value="formState.name" placeholder="请输入险种名称" />
|
||||
<a-input v-model="formState.name" placeholder="请输入险种名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="适用范围" name="applicable_scope">
|
||||
<a-input v-model:value="formState.applicable_scope" placeholder="请输入适用范围" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="险种描述" name="description">
|
||||
<a-textarea
|
||||
v-model:value="formState.description"
|
||||
placeholder="请输入险种描述"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="体验期" name="experience_period">
|
||||
<a-input-number
|
||||
v-model:value="formState.experience_period"
|
||||
placeholder="请输入体验期(月)"
|
||||
:min="0"
|
||||
<a-form-item label="适用生资" name="applicable_livestock">
|
||||
<a-select
|
||||
v-model="formState.applicable_livestock"
|
||||
placeholder="请选择适用生资"
|
||||
style="width: 100%"
|
||||
addon-after="个月"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="保险期间" name="insurance_period">
|
||||
<a-input-number
|
||||
v-model:value="formState.insurance_period"
|
||||
placeholder="请输入保险期间(月)"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
addon-after="个月"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="保费价格" name="premium_price">
|
||||
<a-input-number
|
||||
v-model:value="formState.premium_price"
|
||||
placeholder="请输入保费价格"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
addon-before="¥"
|
||||
/>
|
||||
mode="multiple"
|
||||
:max-tag-count="3"
|
||||
>
|
||||
<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-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-option value="兔">兔</a-select-option>
|
||||
<a-select-option value="鱼">鱼</a-select-option>
|
||||
<a-select-option value="虾">虾</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="服务区域" name="service_area">
|
||||
<a-input v-model:value="formState.service_area" placeholder="请输入服务区域" />
|
||||
<a-col :span="8">
|
||||
<a-form-item label="保险期限/月" name="insurance_term">
|
||||
<a-select
|
||||
v-model="formState.insurance_term"
|
||||
placeholder="请选择保险期限"
|
||||
style="width: 100%"
|
||||
>
|
||||
<a-select-option :value="1">1个月</a-select-option>
|
||||
<a-select-option :value="3">3个月</a-select-option>
|
||||
<a-select-option :value="6">6个月</a-select-option>
|
||||
<a-select-option :value="12">12个月</a-select-option>
|
||||
<a-select-option :value="24">24个月</a-select-option>
|
||||
<a-select-option :value="36">36个月</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="上架时间" name="product_time">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="服务区域" name="description">
|
||||
<a-input v-model="formState.description" placeholder="请输入服务区域" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="保单形式" name="policy_form">
|
||||
<a-select
|
||||
v-model="formState.policy_form"
|
||||
placeholder="请选择保单形式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<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-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="保险额度" name="coverage_amount">
|
||||
<a-input-group compact>
|
||||
<a-input-number
|
||||
v-model="formState.coverage_amount_min"
|
||||
placeholder="最低额度"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 45%"
|
||||
addon-before="¥"
|
||||
/>
|
||||
<a-input
|
||||
style="width: 10%; text-align: center; pointer-events: none"
|
||||
placeholder="~"
|
||||
disabled
|
||||
/>
|
||||
<a-input-number
|
||||
v-model="formState.coverage_amount_max"
|
||||
placeholder="最高额度"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 45%"
|
||||
addon-before="¥"
|
||||
/>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="添加时间" name="created_at">
|
||||
<a-date-picker
|
||||
v-model:value="formState.product_time"
|
||||
placeholder="请选择上架时间"
|
||||
v-model="formState.created_at"
|
||||
placeholder="请选择添加时间"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
show-time
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="在线状态" name="online_status">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="在售状态" name="on_sale_status">
|
||||
<a-switch
|
||||
v-model:checked="formState.online_status"
|
||||
checked-children="在线"
|
||||
un-checked-children="离线"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="排序" name="sort_order">
|
||||
<a-input-number
|
||||
v-model:value="formState.sort_order"
|
||||
placeholder="请输入排序值"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
v-model="formState.on_sale_status"
|
||||
checked-children="在售"
|
||||
un-checked-children="停售"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="备注" name="remarks">
|
||||
<a-textarea
|
||||
v-model:value="formState.remarks"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 详情查看模态框 -->
|
||||
<a-modal
|
||||
:open="detailVisible"
|
||||
@update:open="detailVisible = $event"
|
||||
title="险种详情"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="险种名称">
|
||||
{{ currentDetail.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="适用生资">
|
||||
{{ formatApplicableLivestock(currentDetail.applicable_livestock) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="保险期限">
|
||||
{{ currentDetail.insurance_term ? `${currentDetail.insurance_term}个月` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="服务区域">
|
||||
{{ currentDetail.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="保单形式">
|
||||
{{ currentDetail.policy_form || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="保险额度">
|
||||
{{ formatCoverageAmount(currentDetail.coverage_amount_min, currentDetail.coverage_amount_max) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="添加时间">
|
||||
{{ currentDetail.created_at ? formatDate(currentDetail.created_at) : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="在售状态">
|
||||
<a-tag :color="currentDetail.on_sale_status ? 'green' : 'red'">
|
||||
{{ currentDetail.on_sale_status ? '在售' : '停售' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最后更新时间" :span="2">
|
||||
{{ currentDetail.updated_at ? formatDate(currentDetail.updated_at) : '-' }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -247,30 +276,27 @@ import {
|
||||
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const typeList = ref([])
|
||||
const formRef = ref()
|
||||
const currentDetail = ref({})
|
||||
|
||||
const searchForm = reactive({
|
||||
name: '',
|
||||
applicable_scope: '',
|
||||
service_area: '',
|
||||
online_status: ''
|
||||
name: ''
|
||||
})
|
||||
|
||||
const formState = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
applicable_livestock: [],
|
||||
insurance_term: null,
|
||||
description: '',
|
||||
applicable_scope: '',
|
||||
experience_period: null,
|
||||
insurance_period: null,
|
||||
premium_price: null,
|
||||
service_area: '',
|
||||
product_time: null,
|
||||
online_status: true,
|
||||
sort_order: 0,
|
||||
remarks: ''
|
||||
policy_form: '',
|
||||
coverage_amount_min: null,
|
||||
coverage_amount_max: null,
|
||||
created_at: null,
|
||||
on_sale_status: true
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
@@ -291,45 +317,52 @@ const columns = [
|
||||
fixed: 'left'
|
||||
},
|
||||
{
|
||||
title: '适用范围',
|
||||
dataIndex: 'applicable_scope',
|
||||
key: 'applicable_scope',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '体验期',
|
||||
dataIndex: 'experience_period',
|
||||
key: 'experience_period',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '保险期间',
|
||||
dataIndex: 'insurance_period',
|
||||
key: 'insurance_period',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '保费价格',
|
||||
dataIndex: 'premium_price',
|
||||
key: 'premium_price',
|
||||
title: '适用生资',
|
||||
dataIndex: 'applicable_livestock',
|
||||
key: 'applicable_livestock',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '保险期限/月',
|
||||
dataIndex: 'insurance_term',
|
||||
key: 'insurance_term',
|
||||
width: 120,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '服务区域',
|
||||
dataIndex: 'service_area',
|
||||
key: 'service_area',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '保单形式',
|
||||
dataIndex: 'policy_form',
|
||||
key: 'policy_form',
|
||||
width: 150,
|
||||
filters: [
|
||||
{ text: '电子保单', value: '电子保单' },
|
||||
{ text: '纸质保单', value: '纸质保单' },
|
||||
{ text: '电子保单+纸质保单', value: '电子保单+纸质保单' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '保险额度',
|
||||
dataIndex: 'coverage_amount',
|
||||
key: 'coverage_amount',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '上架时间',
|
||||
dataIndex: 'product_time',
|
||||
key: 'product_time',
|
||||
width: 180
|
||||
title: '添加时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '在线状态',
|
||||
dataIndex: 'online_status',
|
||||
key: 'online_status',
|
||||
title: '在售状态',
|
||||
dataIndex: 'on_sale_status',
|
||||
key: 'on_sale_status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
@@ -342,11 +375,12 @@ const columns = [
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入险种名称' }],
|
||||
applicable_scope: [{ required: true, message: '请输入适用范围' }],
|
||||
experience_period: [{ required: true, message: '请输入体验期' }],
|
||||
insurance_period: [{ required: true, message: '请输入保险期间' }],
|
||||
premium_price: [{ required: true, message: '请输入保费价格' }],
|
||||
service_area: [{ required: true, message: '请输入服务区域' }]
|
||||
applicable_livestock: [{ required: true, message: '请输入适用生资' }],
|
||||
insurance_term: [{ required: true, message: '请输入保险期限' }],
|
||||
description: [{ required: true, message: '请输入服务区域' }],
|
||||
policy_form: [{ required: true, message: '请选择保单形式' }],
|
||||
coverage_amount_min: [{ required: true, message: '请输入最低保险额度' }],
|
||||
coverage_amount_max: [{ required: true, message: '请输入最高保险额度' }]
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
@@ -355,6 +389,38 @@ const formatDate = (date) => {
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
// 格式化保险额度
|
||||
const formatCoverageAmount = (min, max) => {
|
||||
if (!min && !max) return '-'
|
||||
if (min && max) {
|
||||
return `${min}~${max}元`
|
||||
}
|
||||
if (min) return `${min}元以上`
|
||||
if (max) return `${max}元以下`
|
||||
return '-'
|
||||
}
|
||||
|
||||
// 格式化适用生资
|
||||
const formatApplicableLivestock = (livestock) => {
|
||||
if (!livestock) return '-'
|
||||
if (Array.isArray(livestock)) {
|
||||
return livestock.join('、')
|
||||
}
|
||||
if (typeof livestock === 'string') {
|
||||
// 如果是字符串,尝试解析为数组
|
||||
try {
|
||||
const parsed = JSON.parse(livestock)
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.join('、')
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果解析失败,按逗号分割
|
||||
return livestock.split(',').join('、')
|
||||
}
|
||||
}
|
||||
return livestock.toString()
|
||||
}
|
||||
|
||||
const loadInsuranceTypes = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -391,10 +457,7 @@ const handleSearch = () => {
|
||||
|
||||
const resetSearch = () => {
|
||||
Object.assign(searchForm, {
|
||||
name: '',
|
||||
applicable_scope: '',
|
||||
service_area: '',
|
||||
online_status: ''
|
||||
name: ''
|
||||
})
|
||||
handleSearch()
|
||||
}
|
||||
@@ -410,42 +473,54 @@ const showModal = () => {
|
||||
Object.assign(formState, {
|
||||
id: null,
|
||||
name: '',
|
||||
applicable_livestock: [],
|
||||
insurance_term: null,
|
||||
description: '',
|
||||
applicable_scope: '',
|
||||
experience_period: null,
|
||||
insurance_period: null,
|
||||
premium_price: null,
|
||||
service_area: '',
|
||||
product_time: null,
|
||||
online_status: true,
|
||||
sort_order: 0,
|
||||
remarks: ''
|
||||
policy_form: '',
|
||||
coverage_amount_min: null,
|
||||
coverage_amount_max: null,
|
||||
created_at: null,
|
||||
on_sale_status: true
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
isEdit.value = true
|
||||
|
||||
// 处理 applicable_livestock 数据类型
|
||||
let applicableLivestock = []
|
||||
if (record.applicable_livestock) {
|
||||
if (Array.isArray(record.applicable_livestock)) {
|
||||
applicableLivestock = record.applicable_livestock
|
||||
} else if (typeof record.applicable_livestock === 'string') {
|
||||
try {
|
||||
applicableLivestock = JSON.parse(record.applicable_livestock)
|
||||
} catch (e) {
|
||||
applicableLivestock = record.applicable_livestock.split(',').map(item => item.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(formState, {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
applicable_livestock: applicableLivestock,
|
||||
insurance_term: record.insurance_term,
|
||||
description: record.description,
|
||||
applicable_scope: record.applicable_scope,
|
||||
experience_period: record.experience_period,
|
||||
insurance_period: record.insurance_period,
|
||||
premium_price: record.premium_price,
|
||||
service_area: record.service_area,
|
||||
product_time: record.product_time ? dayjs(record.product_time) : null,
|
||||
online_status: record.online_status,
|
||||
sort_order: record.sort_order,
|
||||
remarks: record.remarks
|
||||
policy_form: record.policy_form,
|
||||
coverage_amount_min: record.coverage_amount_min,
|
||||
coverage_amount_max: record.coverage_amount_max,
|
||||
created_at: record.created_at ? dayjs(record.created_at) : null,
|
||||
on_sale_status: record.on_sale_status
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleView = (record) => {
|
||||
// 查看详情功能
|
||||
message.info('查看详情功能待实现')
|
||||
// 设置详情数据
|
||||
currentDetail.value = { ...record }
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -454,7 +529,10 @@ const handleSubmit = async () => {
|
||||
|
||||
const submitData = {
|
||||
...formState,
|
||||
product_time: formState.product_time ? formState.product_time.format('YYYY-MM-DD HH:mm:ss') : null
|
||||
applicable_livestock: Array.isArray(formState.applicable_livestock)
|
||||
? formState.applicable_livestock.join(',')
|
||||
: formState.applicable_livestock,
|
||||
created_at: formState.created_at ? formState.created_at.format('YYYY-MM-DD HH:mm:ss') : null
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
@@ -488,21 +566,21 @@ const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const handleToggleOnlineStatus = async (record, checked) => {
|
||||
const handleToggleOnSaleStatus = async (record, checked) => {
|
||||
try {
|
||||
const response = await insuranceTypeAPI.updateStatus(record.id, {
|
||||
online_status: checked
|
||||
on_sale_status: checked
|
||||
})
|
||||
|
||||
if (response.status === 'success') {
|
||||
message.success('在线状态更新成功')
|
||||
message.success('在售状态更新成功')
|
||||
loadInsuranceTypes()
|
||||
} else {
|
||||
message.error(response.message || '在线状态更新失败')
|
||||
message.error(response.message || '在售状态更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('在线状态更新失败')
|
||||
console.error('Error updating online status:', error)
|
||||
message.error('在售状态更新失败')
|
||||
console.error('Error updating on sale status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
:column="2"
|
||||
>
|
||||
<a-descriptions-item label="保单号">{{ currentPolicy.policy_number }}</a-descriptions-item>
|
||||
<a-descriptions-item label="保险类型">{{ currentPolicy.insurance_type_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="保险类型">{{ currentPolicy.insurance_type?.name || currentPolicy.insurance_type_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="投保人">{{ currentPolicy.policyholder_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="被保险人">{{ currentPolicy.insured_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="保费金额">¥{{ currentPolicy.premium_amount?.toLocaleString() }}</a-descriptions-item>
|
||||
@@ -353,7 +353,10 @@ const columns = [
|
||||
{
|
||||
title: '保险类型',
|
||||
dataIndex: 'insurance_type_name',
|
||||
key: 'insurance_type_name'
|
||||
key: 'insurance_type_name',
|
||||
customRender: ({ record }) => {
|
||||
return record.insurance_type?.name || record.insurance_type_name
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '投保人',
|
||||
|
||||
@@ -214,6 +214,84 @@ const swaggerDefinition = {
|
||||
updated_at: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
},
|
||||
InsuranceType: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'integer', description: '险种ID' },
|
||||
name: { type: 'string', description: '险种名称' },
|
||||
description: { type: 'string', description: '险种描述' },
|
||||
applicable_livestock: { type: 'string', description: '适用牲畜类型' },
|
||||
insurance_term: { type: 'integer', description: '保险期限(月)' },
|
||||
policy_form: { type: 'string', description: '保单形式' },
|
||||
coverage_amount_min: { type: 'number', format: 'decimal', description: '最低保额' },
|
||||
coverage_amount_max: { type: 'number', format: 'decimal', description: '最高保额' },
|
||||
premium_rate: { type: 'number', format: 'decimal', description: '保险费率' },
|
||||
service_area: { type: 'string', description: '服务区域' },
|
||||
add_time: { type: 'string', format: 'date-time', description: '添加时间' },
|
||||
on_sale_status: { type: 'boolean', description: '在售状态' },
|
||||
sort_order: { type: 'integer', description: '排序顺序' },
|
||||
remarks: { type: 'string', description: '备注' },
|
||||
status: { type: 'string', enum: ['active', 'inactive'], description: '险种状态' },
|
||||
created_by: { type: 'integer', description: '创建人ID' },
|
||||
updated_by: { type: 'integer', description: '更新人ID' },
|
||||
created_at: { type: 'string', format: 'date-time', description: '创建时间' },
|
||||
updated_at: { type: 'string', format: 'date-time', description: '更新时间' }
|
||||
}
|
||||
},
|
||||
CreateInsuranceTypeRequest: {
|
||||
type: 'object',
|
||||
required: ['name', 'coverage_amount_min', 'coverage_amount_max', 'premium_rate'],
|
||||
properties: {
|
||||
name: { type: 'string', description: '险种名称', minLength: 1, maxLength: 100 },
|
||||
description: { type: 'string', description: '险种描述' },
|
||||
applicable_livestock: { type: 'string', description: '适用牲畜类型', maxLength: 100 },
|
||||
insurance_term: { type: 'integer', description: '保险期限(月)', minimum: 1 },
|
||||
policy_form: { type: 'string', description: '保单形式', maxLength: 50 },
|
||||
coverage_amount_min: { type: 'number', format: 'decimal', description: '最低保额', minimum: 0 },
|
||||
coverage_amount_max: { type: 'number', format: 'decimal', description: '最高保额', minimum: 0 },
|
||||
premium_rate: { type: 'number', format: 'decimal', description: '保险费率', minimum: 0, maximum: 1 },
|
||||
service_area: { type: 'string', description: '服务区域' },
|
||||
add_time: { type: 'string', format: 'date-time', description: '添加时间' },
|
||||
on_sale_status: { type: 'boolean', description: '在售状态', default: true },
|
||||
sort_order: { type: 'integer', description: '排序顺序', default: 0 },
|
||||
remarks: { type: 'string', description: '备注' },
|
||||
status: { type: 'string', enum: ['active', 'inactive'], description: '险种状态', default: 'active' }
|
||||
}
|
||||
},
|
||||
UpdateInsuranceTypeRequest: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: '险种名称', minLength: 1, maxLength: 100 },
|
||||
description: { type: 'string', description: '险种描述' },
|
||||
applicable_livestock: { type: 'string', description: '适用牲畜类型', maxLength: 100 },
|
||||
insurance_term: { type: 'integer', description: '保险期限(月)', minimum: 1 },
|
||||
policy_form: { type: 'string', description: '保单形式', maxLength: 50 },
|
||||
coverage_amount_min: { type: 'number', format: 'decimal', description: '最低保额', minimum: 0 },
|
||||
coverage_amount_max: { type: 'number', format: 'decimal', description: '最高保额', minimum: 0 },
|
||||
premium_rate: { type: 'number', format: 'decimal', description: '保险费率', minimum: 0, maximum: 1 },
|
||||
service_area: { type: 'string', description: '服务区域' },
|
||||
add_time: { type: 'string', format: 'date-time', description: '添加时间' },
|
||||
on_sale_status: { type: 'boolean', description: '在售状态' },
|
||||
sort_order: { type: 'integer', description: '排序顺序' },
|
||||
remarks: { type: 'string', description: '备注' },
|
||||
status: { type: 'string', enum: ['active', 'inactive'], description: '险种状态' }
|
||||
}
|
||||
},
|
||||
UpdateInsuranceTypeStatusRequest: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: { type: 'string', enum: ['active', 'inactive'], description: '险种状态' },
|
||||
on_sale_status: { type: 'boolean', description: '在售状态' }
|
||||
}
|
||||
},
|
||||
Pagination: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: { type: 'integer', description: '当前页码' },
|
||||
limit: { type: 'integer', description: '每页数量' },
|
||||
total: { type: 'integer', description: '总记录数' }
|
||||
}
|
||||
},
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -293,6 +371,7 @@ const swaggerDefinition = {
|
||||
tags: [
|
||||
{ name: '认证', description: '用户认证相关接口' },
|
||||
{ name: '用户管理', description: '用户管理相关接口' },
|
||||
{ name: '险种管理', description: '保险险种管理相关接口' },
|
||||
{ name: '保险申请', description: '保险申请管理相关接口' },
|
||||
{ name: '保单管理', description: '保单管理相关接口' },
|
||||
{ name: '理赔管理', description: '理赔管理相关接口' },
|
||||
|
||||
@@ -5,7 +5,15 @@ const responseFormat = require('../utils/response');
|
||||
// 获取保险类型列表
|
||||
const getInsuranceTypes = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, name, status } = req.query;
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
name,
|
||||
status,
|
||||
applicable_livestock,
|
||||
service_area,
|
||||
on_sale_status
|
||||
} = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const whereClause = {};
|
||||
@@ -15,12 +23,21 @@ const getInsuranceTypes = async (req, res) => {
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
if (applicable_livestock) {
|
||||
whereClause.applicable_livestock = { [Op.like]: `%${applicable_livestock}%` };
|
||||
}
|
||||
if (service_area) {
|
||||
whereClause.service_area = { [Op.like]: `%${service_area}%` };
|
||||
}
|
||||
if (on_sale_status !== undefined && on_sale_status !== '') {
|
||||
whereClause.on_sale_status = on_sale_status === 'true';
|
||||
}
|
||||
|
||||
const { count, rows } = await InsuranceType.findAndCountAll({
|
||||
where: whereClause,
|
||||
limit: parseInt(pageSize),
|
||||
offset: offset,
|
||||
order: [['created_at', 'DESC']]
|
||||
order: [['add_time', 'DESC'], ['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json(responseFormat.pagination(rows, {
|
||||
@@ -57,9 +74,17 @@ const createInsuranceType = async (req, res) => {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
applicable_livestock,
|
||||
insurance_term,
|
||||
policy_form,
|
||||
coverage_amount_min,
|
||||
coverage_amount_max,
|
||||
premium_rate,
|
||||
service_area,
|
||||
add_time,
|
||||
on_sale_status = true,
|
||||
sort_order = 0,
|
||||
remarks,
|
||||
status = 'active'
|
||||
} = req.body;
|
||||
|
||||
@@ -72,9 +97,17 @@ const createInsuranceType = async (req, res) => {
|
||||
const insuranceType = await InsuranceType.create({
|
||||
name,
|
||||
description,
|
||||
applicable_livestock,
|
||||
insurance_term,
|
||||
policy_form,
|
||||
coverage_amount_min,
|
||||
coverage_amount_max,
|
||||
premium_rate,
|
||||
service_area,
|
||||
add_time: add_time || new Date(),
|
||||
on_sale_status,
|
||||
sort_order,
|
||||
remarks,
|
||||
status,
|
||||
created_by: req.user?.id
|
||||
});
|
||||
@@ -97,9 +130,17 @@ const updateInsuranceType = async (req, res) => {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
applicable_livestock,
|
||||
insurance_term,
|
||||
policy_form,
|
||||
coverage_amount_min,
|
||||
coverage_amount_max,
|
||||
premium_rate,
|
||||
service_area,
|
||||
add_time,
|
||||
on_sale_status,
|
||||
sort_order,
|
||||
remarks,
|
||||
status
|
||||
} = req.body;
|
||||
|
||||
@@ -123,10 +164,18 @@ const updateInsuranceType = async (req, res) => {
|
||||
|
||||
await insuranceType.update({
|
||||
name: name || insuranceType.name,
|
||||
description: description || insuranceType.description,
|
||||
coverage_amount_min: coverage_amount_min || insuranceType.coverage_amount_min,
|
||||
coverage_amount_max: coverage_amount_max || insuranceType.coverage_amount_max,
|
||||
premium_rate: premium_rate || insuranceType.premium_rate,
|
||||
description: description !== undefined ? description : insuranceType.description,
|
||||
applicable_livestock: applicable_livestock !== undefined ? applicable_livestock : insuranceType.applicable_livestock,
|
||||
insurance_term: insurance_term !== undefined ? insurance_term : insuranceType.insurance_term,
|
||||
policy_form: policy_form !== undefined ? policy_form : insuranceType.policy_form,
|
||||
coverage_amount_min: coverage_amount_min !== undefined ? coverage_amount_min : insuranceType.coverage_amount_min,
|
||||
coverage_amount_max: coverage_amount_max !== undefined ? coverage_amount_max : insuranceType.coverage_amount_max,
|
||||
premium_rate: premium_rate !== undefined ? premium_rate : insuranceType.premium_rate,
|
||||
service_area: service_area !== undefined ? service_area : insuranceType.service_area,
|
||||
add_time: add_time !== undefined ? add_time : insuranceType.add_time,
|
||||
on_sale_status: on_sale_status !== undefined ? on_sale_status : insuranceType.on_sale_status,
|
||||
sort_order: sort_order !== undefined ? sort_order : insuranceType.sort_order,
|
||||
remarks: remarks !== undefined ? remarks : insuranceType.remarks,
|
||||
status: status || insuranceType.status,
|
||||
updated_by: req.user?.id
|
||||
});
|
||||
@@ -152,16 +201,22 @@ const deleteInsuranceType = async (req, res) => {
|
||||
return res.status(404).json(responseFormat.error('险种不存在'));
|
||||
}
|
||||
|
||||
// 检查是否有相关的保险申请或保单
|
||||
// const hasApplications = await insuranceType.countInsuranceApplications();
|
||||
// if (hasApplications > 0) {
|
||||
// return res.status(400).json(responseFormat.error('该险种下存在保险申请,无法删除'));
|
||||
// }
|
||||
// 检查是否有相关的保险申请
|
||||
const hasApplications = await insuranceType.countApplications();
|
||||
if (hasApplications > 0) {
|
||||
return res.status(400).json(responseFormat.error('该险种下存在保险申请,无法删除', 400));
|
||||
}
|
||||
|
||||
await insuranceType.destroy();
|
||||
res.json(responseFormat.success(null, '删除险种成功'));
|
||||
} catch (error) {
|
||||
console.error('删除险种错误:', error);
|
||||
|
||||
// 处理外键约束错误
|
||||
if (error.name === 'SequelizeForeignKeyConstraintError') {
|
||||
return res.status(400).json(responseFormat.error('该险种下存在相关数据,无法删除', 400));
|
||||
}
|
||||
|
||||
res.status(500).json(responseFormat.error('删除险种失败'));
|
||||
}
|
||||
};
|
||||
@@ -170,14 +225,18 @@ const deleteInsuranceType = async (req, res) => {
|
||||
const updateInsuranceTypeStatus = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
const { status, on_sale_status } = req.body;
|
||||
|
||||
const insuranceType = await InsuranceType.findByPk(id);
|
||||
if (!insuranceType) {
|
||||
return res.status(404).json(responseFormat.error('险种不存在'));
|
||||
}
|
||||
|
||||
await insuranceType.update({ status, updated_by: req.user?.id });
|
||||
const updateData = { updated_by: req.user?.id };
|
||||
if (status !== undefined) updateData.status = status;
|
||||
if (on_sale_status !== undefined) updateData.on_sale_status = on_sale_status;
|
||||
|
||||
await insuranceType.update(updateData);
|
||||
res.json(responseFormat.success(insuranceType, '更新险种状态成功'));
|
||||
} catch (error) {
|
||||
console.error('更新险种状态错误:', error);
|
||||
|
||||
@@ -8,10 +8,35 @@ const { Op } = require('sequelize');
|
||||
// 获取生资理赔列表
|
||||
const getLivestockClaims = async (req, res) => {
|
||||
try {
|
||||
console.log('🚀 [后端] 开始处理理赔列表查询请求');
|
||||
console.log('📥 [后端] 接收到的查询参数:', {
|
||||
接收时间: new Date().toLocaleString(),
|
||||
完整URL: req.originalUrl,
|
||||
查询字符串: req.url,
|
||||
原始查询参数: JSON.stringify(req.query, null, 2),
|
||||
查询参数类型: typeof req.query,
|
||||
查询参数键: Object.keys(req.query),
|
||||
请求方法: req.method,
|
||||
请求路径: req.path,
|
||||
用户信息: req.user ? { id: req.user.id, username: req.user.username } : '未登录'
|
||||
});
|
||||
|
||||
console.log('🔍 [详细调试] 请求信息:');
|
||||
console.log('req.url:', req.url);
|
||||
console.log('req.originalUrl:', req.originalUrl);
|
||||
console.log('req.baseUrl:', req.baseUrl);
|
||||
console.log('req.path:', req.path);
|
||||
console.log('查询字符串:', req.url.split('?')[1] || '无');
|
||||
console.log('原始查询参数:', req.query);
|
||||
console.log('查询参数类型:', typeof req.query);
|
||||
console.log('查询参数键:', Object.keys(req.query));
|
||||
console.log('查询参数JSON:', JSON.stringify(req.query));
|
||||
|
||||
const {
|
||||
claim_no,
|
||||
policy_no,
|
||||
farmer_name,
|
||||
reporter_name,
|
||||
contact_phone,
|
||||
claim_status,
|
||||
claim_type,
|
||||
start_date,
|
||||
@@ -20,21 +45,49 @@ const getLivestockClaims = async (req, res) => {
|
||||
limit = 10
|
||||
} = req.query;
|
||||
|
||||
console.log('🔍 [后端] 解析后的查询参数:', {
|
||||
理赔单号: claim_no,
|
||||
保单号: policy_no,
|
||||
报案人: reporter_name,
|
||||
联系电话: contact_phone,
|
||||
理赔状态: claim_status,
|
||||
理赔类型: claim_type,
|
||||
开始日期: start_date,
|
||||
结束日期: end_date,
|
||||
页码: page,
|
||||
每页条数: limit
|
||||
});
|
||||
|
||||
const whereClause = {};
|
||||
|
||||
// 理赔编号筛选
|
||||
// 理赔编号筛选 - 精确匹配
|
||||
if (claim_no) {
|
||||
whereClause.claim_no = { [Op.like]: `%${claim_no}%` };
|
||||
whereClause.claim_no = claim_no;
|
||||
console.log('🔍 [后端] 添加理赔编号筛选条件(精确匹配):', whereClause.claim_no);
|
||||
}
|
||||
|
||||
// 理赔状态筛选
|
||||
if (claim_status) {
|
||||
whereClause.claim_status = claim_status;
|
||||
console.log('🔍 [后端] 添加理赔状态筛选条件:', whereClause.claim_status);
|
||||
}
|
||||
|
||||
// 理赔类型筛选
|
||||
if (claim_type) {
|
||||
whereClause.claim_type = claim_type;
|
||||
console.log('🔍 [后端] 添加理赔类型筛选条件:', whereClause.claim_type);
|
||||
}
|
||||
|
||||
// 报案人筛选
|
||||
if (reporter_name) {
|
||||
whereClause.reporter_name = { [Op.like]: `%${reporter_name}%` };
|
||||
console.log('🔍 [后端] 添加报案人筛选条件:', whereClause.reporter_name);
|
||||
}
|
||||
|
||||
// 联系电话筛选
|
||||
if (contact_phone) {
|
||||
whereClause.contact_phone = { [Op.like]: `%${contact_phone}%` };
|
||||
console.log('🔍 [后端] 添加联系电话筛选条件:', whereClause.contact_phone);
|
||||
}
|
||||
|
||||
// 日期范围筛选
|
||||
@@ -42,17 +95,30 @@ const getLivestockClaims = async (req, res) => {
|
||||
whereClause.incident_date = {
|
||||
[Op.between]: [new Date(start_date), new Date(end_date)]
|
||||
};
|
||||
console.log('🔍 [后端] 添加日期范围筛选条件:', whereClause.incident_date);
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await LivestockClaim.findAndCountAll({
|
||||
console.log('📊 [后端] 构建的查询条件:', {
|
||||
构建时间: new Date().toLocaleString(),
|
||||
WHERE条件: JSON.stringify(whereClause, null, 2),
|
||||
分页信息: {
|
||||
页码: page,
|
||||
每页条数: limit,
|
||||
偏移量: offset
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🗄️ [后端] 开始执行数据库查询...');
|
||||
|
||||
const queryConfig = {
|
||||
where: whereClause,
|
||||
include: [
|
||||
{
|
||||
model: LivestockPolicy,
|
||||
as: 'policy',
|
||||
attributes: ['id', 'policy_no', 'farmer_name', 'farmer_phone', 'livestock_count'],
|
||||
attributes: ['id', 'policy_no', 'policyholder_name', 'policyholder_phone', 'livestock_count'],
|
||||
where: policy_no ? { policy_no: { [Op.like]: `%${policy_no}%` } } : {},
|
||||
required: !!policy_no,
|
||||
include: [
|
||||
@@ -77,23 +143,66 @@ const getLivestockClaims = async (req, res) => {
|
||||
order: [['created_at', 'DESC']],
|
||||
offset,
|
||||
limit: parseInt(limit)
|
||||
};
|
||||
|
||||
console.log('🗄️ [后端] 数据库查询配置:', {
|
||||
查询时间: new Date().toLocaleString(),
|
||||
查询配置: JSON.stringify(queryConfig, null, 2)
|
||||
});
|
||||
|
||||
// 如果有农户姓名筛选,需要在关联查询后再过滤
|
||||
let filteredRows = rows;
|
||||
if (farmer_name) {
|
||||
filteredRows = rows.filter(claim =>
|
||||
claim.policy && claim.policy.farmer_name.includes(farmer_name)
|
||||
);
|
||||
}
|
||||
console.log('🚀 [后端] 即将执行查询,最终配置:', JSON.stringify(queryConfig, null, 2));
|
||||
|
||||
res.json(responseFormat.pagination(filteredRows, {
|
||||
const { count, rows } = await LivestockClaim.findAndCountAll(queryConfig);
|
||||
|
||||
console.log('✅ [后端] 数据库查询完成:', {
|
||||
查询时间: new Date().toLocaleString(),
|
||||
返回总数: count,
|
||||
返回记录数: rows.length,
|
||||
记录ID列表: rows.map(r => ({ id: r.id, claim_no: r.claim_no }))
|
||||
});
|
||||
|
||||
console.log('📋 [后端] 详细查询结果:', {
|
||||
查询完成时间: new Date().toLocaleString(),
|
||||
查询结果统计: {
|
||||
总记录数: count,
|
||||
当前页记录数: rows.length,
|
||||
查询耗时: '已完成'
|
||||
},
|
||||
数据样例: rows.length > 0 ? {
|
||||
第一条记录ID: rows[0].id,
|
||||
第一条记录理赔单号: rows[0].claim_no,
|
||||
第一条记录状态: rows[0].claim_status
|
||||
} : '无数据'
|
||||
});
|
||||
|
||||
const responseData = responseFormat.pagination(rows, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: count
|
||||
}, '获取生资理赔列表成功'));
|
||||
}, '获取生资理赔列表成功');
|
||||
|
||||
console.log('📤 [后端->前端] 准备发送响应:', {
|
||||
响应时间: new Date().toLocaleString(),
|
||||
响应状态: 'success',
|
||||
响应数据结构: {
|
||||
status: responseData.status,
|
||||
message: responseData.message,
|
||||
data类型: Array.isArray(responseData.data) ? 'Array' : typeof responseData.data,
|
||||
data长度: responseData.data?.length,
|
||||
pagination: responseData.pagination
|
||||
},
|
||||
完整响应: JSON.stringify(responseData, null, 2)
|
||||
});
|
||||
|
||||
res.json(responseData);
|
||||
} catch (error) {
|
||||
console.error('获取生资理赔列表错误:', error);
|
||||
console.error('💥 [后端] 获取生资理赔列表错误:', {
|
||||
错误时间: new Date().toLocaleString(),
|
||||
错误类型: error.name,
|
||||
错误信息: error.message,
|
||||
错误堆栈: error.stack,
|
||||
SQL错误: error.sql || '无SQL信息'
|
||||
});
|
||||
res.status(500).json(responseFormat.error('获取生资理赔列表失败'));
|
||||
}
|
||||
};
|
||||
@@ -355,10 +464,88 @@ const getLivestockClaimStats = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 更新生资理赔
|
||||
const updateLivestockClaim = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const claim = await LivestockClaim.findByPk(id);
|
||||
if (!claim) {
|
||||
return res.status(404).json(responseFormat.error('生资理赔不存在'));
|
||||
}
|
||||
|
||||
// 更新理赔信息
|
||||
await claim.update({
|
||||
...updateData,
|
||||
updated_by: req.user?.id
|
||||
});
|
||||
|
||||
// 获取更新后的完整信息
|
||||
const updatedClaim = await LivestockClaim.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: LivestockPolicy,
|
||||
as: 'policy',
|
||||
attributes: ['id', 'policy_no', 'policyholder_name', 'policyholder_phone'],
|
||||
include: [
|
||||
{
|
||||
model: LivestockType,
|
||||
as: 'livestock_type',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'creator',
|
||||
attributes: ['id', 'real_name', 'username']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'reviewer',
|
||||
attributes: ['id', 'real_name', 'username']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
res.json(responseFormat.success(updatedClaim, '生资理赔更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新生资理赔错误:', error);
|
||||
res.status(500).json(responseFormat.error('更新生资理赔失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除生资理赔
|
||||
const deleteLivestockClaim = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const claim = await LivestockClaim.findByPk(id);
|
||||
if (!claim) {
|
||||
return res.status(404).json(responseFormat.error('生资理赔不存在'));
|
||||
}
|
||||
|
||||
// 检查是否可以删除(只有待处理状态的理赔可以删除)
|
||||
if (claim.claim_status !== 'pending') {
|
||||
return res.status(400).json(responseFormat.error('只有待处理状态的理赔可以删除'));
|
||||
}
|
||||
|
||||
await claim.destroy();
|
||||
|
||||
res.json(responseFormat.success(null, '生资理赔删除成功'));
|
||||
} catch (error) {
|
||||
console.error('删除生资理赔错误:', error);
|
||||
res.status(500).json(responseFormat.error('删除生资理赔失败'));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getLivestockClaims,
|
||||
createLivestockClaim,
|
||||
getLivestockClaimById,
|
||||
updateLivestockClaim,
|
||||
deleteLivestockClaim,
|
||||
reviewLivestockClaim,
|
||||
updateLivestockClaimPayment,
|
||||
getLivestockClaimStats
|
||||
|
||||
308
insurance_backend/docs/insurance-type-api-examples.md
Normal file
308
insurance_backend/docs/insurance-type-api-examples.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# 险种管理API使用示例
|
||||
|
||||
## 概述
|
||||
|
||||
本文档提供了险种管理API的详细使用示例,包括请求格式、响应格式和错误处理。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础URL**: `http://localhost:3000/api`
|
||||
- **认证方式**: Bearer Token (JWT)
|
||||
- **内容类型**: `application/json`
|
||||
|
||||
## 认证
|
||||
|
||||
所有API请求都需要在请求头中包含有效的JWT令牌:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
## API端点
|
||||
|
||||
### 1. 获取险种列表
|
||||
|
||||
**请求**
|
||||
```http
|
||||
GET /api/insurance-types?page=1&pageSize=10&name=牛&status=active
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取险种列表成功",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "牛只意外伤害保险",
|
||||
"description": "为牛只提供意外伤害保障",
|
||||
"applicable_livestock": "牛",
|
||||
"insurance_term": "1年",
|
||||
"policy_form": "团体保险",
|
||||
"on_sale_status": true,
|
||||
"coverage_amount_min": 1000.00,
|
||||
"coverage_amount_max": 50000.00,
|
||||
"premium_rate": 0.05,
|
||||
"status": "active",
|
||||
"add_time": "2024-01-15T08:30:00.000Z",
|
||||
"created_at": "2024-01-15T08:30:00.000Z",
|
||||
"updated_at": "2024-01-15T08:30:00.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"total": 1,
|
||||
"totalPages": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取单个险种详情
|
||||
|
||||
**请求**
|
||||
```http
|
||||
GET /api/insurance-types/1
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "获取险种详情成功",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "牛只意外伤害保险",
|
||||
"description": "为牛只提供意外伤害保障",
|
||||
"applicable_livestock": "牛",
|
||||
"insurance_term": "1年",
|
||||
"policy_form": "团体保险",
|
||||
"on_sale_status": true,
|
||||
"coverage_amount_min": 1000.00,
|
||||
"coverage_amount_max": 50000.00,
|
||||
"premium_rate": 0.05,
|
||||
"status": "active",
|
||||
"add_time": "2024-01-15T08:30:00.000Z",
|
||||
"created_at": "2024-01-15T08:30:00.000Z",
|
||||
"updated_at": "2024-01-15T08:30:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 创建险种
|
||||
|
||||
**请求**
|
||||
```http
|
||||
POST /api/insurance-types
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "羊只疾病保险",
|
||||
"description": "为羊只提供疾病保障",
|
||||
"applicable_livestock": "羊",
|
||||
"insurance_term": "1年",
|
||||
"policy_form": "个体保险",
|
||||
"coverage_amount_min": 500.00,
|
||||
"coverage_amount_max": 20000.00,
|
||||
"premium_rate": 0.08
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "创建险种成功",
|
||||
"data": {
|
||||
"id": 2,
|
||||
"name": "羊只疾病保险",
|
||||
"description": "为羊只提供疾病保障",
|
||||
"applicable_livestock": "羊",
|
||||
"insurance_term": "1年",
|
||||
"policy_form": "个体保险",
|
||||
"on_sale_status": false,
|
||||
"coverage_amount_min": 500.00,
|
||||
"coverage_amount_max": 20000.00,
|
||||
"premium_rate": 0.08,
|
||||
"status": "active",
|
||||
"add_time": "2024-01-15T09:00:00.000Z",
|
||||
"created_at": "2024-01-15T09:00:00.000Z",
|
||||
"updated_at": "2024-01-15T09:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 更新险种
|
||||
|
||||
**请求**
|
||||
```http
|
||||
PUT /api/insurance-types/2
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "羊只综合保险",
|
||||
"description": "为羊只提供疾病和意外伤害综合保障",
|
||||
"premium_rate": 0.10
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "更新险种成功",
|
||||
"data": {
|
||||
"id": 2,
|
||||
"name": "羊只综合保险",
|
||||
"description": "为羊只提供疾病和意外伤害综合保障",
|
||||
"applicable_livestock": "羊",
|
||||
"insurance_term": "1年",
|
||||
"policy_form": "个体保险",
|
||||
"on_sale_status": false,
|
||||
"coverage_amount_min": 500.00,
|
||||
"coverage_amount_max": 20000.00,
|
||||
"premium_rate": 0.10,
|
||||
"status": "active",
|
||||
"add_time": "2024-01-15T09:00:00.000Z",
|
||||
"created_at": "2024-01-15T09:00:00.000Z",
|
||||
"updated_at": "2024-01-15T09:15:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 更新险种状态
|
||||
|
||||
**请求**
|
||||
```http
|
||||
PATCH /api/insurance-types/2/status
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "active",
|
||||
"on_sale_status": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "更新险种状态成功",
|
||||
"data": {
|
||||
"id": 2,
|
||||
"name": "羊只综合保险",
|
||||
"description": "为羊只提供疾病和意外伤害综合保障",
|
||||
"applicable_livestock": "羊",
|
||||
"insurance_term": "1年",
|
||||
"policy_form": "个体保险",
|
||||
"on_sale_status": true,
|
||||
"coverage_amount_min": 500.00,
|
||||
"coverage_amount_max": 20000.00,
|
||||
"premium_rate": 0.10,
|
||||
"status": "active",
|
||||
"add_time": "2024-01-15T09:00:00.000Z",
|
||||
"created_at": "2024-01-15T09:00:00.000Z",
|
||||
"updated_at": "2024-01-15T09:20:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 删除险种
|
||||
|
||||
**请求**
|
||||
```http
|
||||
DELETE /api/insurance-types/2
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "删除险种成功"
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误响应
|
||||
|
||||
#### 401 未授权
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "未授权访问",
|
||||
"error": "Unauthorized"
|
||||
}
|
||||
```
|
||||
|
||||
#### 403 权限不足
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "权限不足",
|
||||
"error": "Forbidden"
|
||||
}
|
||||
```
|
||||
|
||||
#### 404 资源不存在
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "险种不存在",
|
||||
"error": "Not Found"
|
||||
}
|
||||
```
|
||||
|
||||
#### 400 请求参数错误
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "险种名称已存在",
|
||||
"error": "Bad Request"
|
||||
}
|
||||
```
|
||||
|
||||
#### 500 服务器内部错误
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "服务器内部错误",
|
||||
"error": "Internal Server Error"
|
||||
}
|
||||
```
|
||||
|
||||
## 查询参数说明
|
||||
|
||||
### 获取险种列表支持的查询参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| page | integer | 否 | 1 | 页码,最小值为1 |
|
||||
| pageSize | integer | 否 | 10 | 每页数量,范围1-100 |
|
||||
| name | string | 否 | - | 险种名称,支持模糊搜索 |
|
||||
| status | string | 否 | - | 险种状态,可选值:active, inactive |
|
||||
| applicable_livestock | string | 否 | - | 适用牲畜类型,支持模糊搜索 |
|
||||
| service_area | string | 否 | - | 服务区域,支持模糊搜索 |
|
||||
| on_sale_status | boolean | 否 | - | 在售状态,true或false |
|
||||
|
||||
## 使用注意事项
|
||||
|
||||
1. **认证令牌**: 所有请求都需要有效的JWT令牌
|
||||
2. **权限检查**: 用户需要具有相应的险种管理权限
|
||||
3. **数据验证**: 创建和更新时会进行数据格式验证
|
||||
4. **唯一性检查**: 险种名称必须唯一
|
||||
5. **分页限制**: 每页最多返回100条记录
|
||||
6. **状态管理**: 险种状态和在售状态可以独立管理
|
||||
|
||||
## 测试工具推荐
|
||||
|
||||
- **Postman**: 用于API测试和调试
|
||||
- **curl**: 命令行工具,适合脚本化测试
|
||||
- **Swagger UI**: 在线API文档和测试界面 (http://localhost:3000/api-docs)
|
||||
690
insurance_backend/docs/insurance-type-api.yaml
Normal file
690
insurance_backend/docs/insurance-type-api.yaml
Normal file
@@ -0,0 +1,690 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: 险种管理API
|
||||
description: 保险系统险种管理功能相关API接口文档
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: 开发团队
|
||||
email: dev@example.com
|
||||
|
||||
servers:
|
||||
- url: http://localhost:3000/api
|
||||
description: 开发环境
|
||||
|
||||
paths:
|
||||
/insurance-types:
|
||||
get:
|
||||
summary: 获取险种列表
|
||||
description: 分页获取险种列表,支持多种筛选条件
|
||||
tags:
|
||||
- 险种管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: 页码
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 1
|
||||
minimum: 1
|
||||
- name: pageSize
|
||||
in: query
|
||||
description: 每页数量
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
- name: name
|
||||
in: query
|
||||
description: 险种名称(模糊搜索)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: status
|
||||
in: query
|
||||
description: 险种状态
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [active, inactive]
|
||||
- name: applicable_livestock
|
||||
in: query
|
||||
description: 适用牲畜类型(模糊搜索)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: service_area
|
||||
in: query
|
||||
description: 服务区域(模糊搜索)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: on_sale_status
|
||||
in: query
|
||||
description: 在售状态
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
example: "获取险种列表成功"
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/InsuranceType'
|
||||
pagination:
|
||||
$ref: '#/components/schemas/Pagination'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
post:
|
||||
summary: 创建险种
|
||||
description: 创建新的保险险种
|
||||
tags:
|
||||
- 险种管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateInsuranceTypeRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: 创建成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
example: "创建险种成功"
|
||||
data:
|
||||
$ref: '#/components/schemas/InsuranceType'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/insurance-types/{id}:
|
||||
get:
|
||||
summary: 获取险种详情
|
||||
description: 根据ID获取单个险种的详细信息
|
||||
tags:
|
||||
- 险种管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: 险种ID
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: 获取成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
example: "获取保险类型详情成功"
|
||||
data:
|
||||
$ref: '#/components/schemas/InsuranceType'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
put:
|
||||
summary: 更新险种
|
||||
description: 更新指定ID的险种信息
|
||||
tags:
|
||||
- 险种管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: 险种ID
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateInsuranceTypeRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 更新成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
example: "更新险种成功"
|
||||
data:
|
||||
$ref: '#/components/schemas/InsuranceType'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
delete:
|
||||
summary: 删除险种
|
||||
description: 删除指定ID的险种
|
||||
tags:
|
||||
- 险种管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: 险种ID
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: 删除成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
example: 200
|
||||
status:
|
||||
type: string
|
||||
example: "success"
|
||||
data:
|
||||
type: null
|
||||
example: null
|
||||
message:
|
||||
type: string
|
||||
example: "删除险种成功"
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2025-09-28T06:28:03.150Z"
|
||||
'400':
|
||||
description: 删除失败 - 外键约束错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: 400
|
||||
status: error
|
||||
data: null
|
||||
message: "该险种下存在保险申请,无法删除"
|
||||
timestamp: "2025-09-28T06:28:02.881Z"
|
||||
'404':
|
||||
description: 险种不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: 404
|
||||
status: error
|
||||
data: null
|
||||
message: "险种不存在"
|
||||
timestamp: "2025-09-28T06:28:02.881Z"
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/insurance-types/{id}/status:
|
||||
patch:
|
||||
summary: 更新险种状态
|
||||
description: 更新指定ID险种的状态或在售状态
|
||||
tags:
|
||||
- 险种管理
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: 险种ID
|
||||
schema:
|
||||
type: integer
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateInsuranceTypeStatusRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: 更新成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
example: "更新险种状态成功"
|
||||
data:
|
||||
$ref: '#/components/schemas/InsuranceType'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
||||
schemas:
|
||||
InsuranceType:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: 险种ID
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
description: 险种名称
|
||||
example: "牛羊保险"
|
||||
description:
|
||||
type: string
|
||||
description: 险种描述
|
||||
example: "针对牛羊等大型牲畜的综合保险产品"
|
||||
applicable_livestock:
|
||||
type: string
|
||||
description: 适用牲畜类型
|
||||
example: "牛、羊"
|
||||
insurance_term:
|
||||
type: integer
|
||||
description: 保险期限(月)
|
||||
example: 12
|
||||
policy_form:
|
||||
type: string
|
||||
description: 保单形式
|
||||
example: "电子保单"
|
||||
coverage_amount_min:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 最低保额
|
||||
example: 1000.00
|
||||
coverage_amount_max:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 最高保额
|
||||
example: 50000.00
|
||||
premium_rate:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 保险费率
|
||||
example: 0.0350
|
||||
service_area:
|
||||
type: string
|
||||
description: 服务区域
|
||||
example: "宁夏回族自治区"
|
||||
add_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 添加时间
|
||||
example: "2024-01-15T10:30:00.000Z"
|
||||
on_sale_status:
|
||||
type: boolean
|
||||
description: 在售状态
|
||||
example: true
|
||||
sort_order:
|
||||
type: integer
|
||||
description: 排序顺序
|
||||
example: 0
|
||||
remarks:
|
||||
type: string
|
||||
description: 备注
|
||||
example: "特殊说明信息"
|
||||
status:
|
||||
type: string
|
||||
enum: [active, inactive]
|
||||
description: 险种状态
|
||||
example: "active"
|
||||
created_by:
|
||||
type: integer
|
||||
description: 创建人ID
|
||||
example: 1
|
||||
updated_by:
|
||||
type: integer
|
||||
description: 更新人ID
|
||||
example: 1
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 创建时间
|
||||
example: "2024-01-15T10:30:00.000Z"
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 更新时间
|
||||
example: "2024-01-15T10:30:00.000Z"
|
||||
|
||||
CreateInsuranceTypeRequest:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- coverage_amount_min
|
||||
- coverage_amount_max
|
||||
- premium_rate
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: 险种名称
|
||||
example: "牛羊保险"
|
||||
minLength: 1
|
||||
maxLength: 100
|
||||
description:
|
||||
type: string
|
||||
description: 险种描述
|
||||
example: "针对牛羊等大型牲畜的综合保险产品"
|
||||
applicable_livestock:
|
||||
type: string
|
||||
description: 适用牲畜类型
|
||||
example: "牛、羊"
|
||||
maxLength: 100
|
||||
insurance_term:
|
||||
type: integer
|
||||
description: 保险期限(月)
|
||||
example: 12
|
||||
minimum: 1
|
||||
policy_form:
|
||||
type: string
|
||||
description: 保单形式
|
||||
example: "电子保单"
|
||||
maxLength: 50
|
||||
coverage_amount_min:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 最低保额
|
||||
example: 1000.00
|
||||
minimum: 0
|
||||
coverage_amount_max:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 最高保额
|
||||
example: 50000.00
|
||||
minimum: 0
|
||||
premium_rate:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 保险费率
|
||||
example: 0.0350
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
service_area:
|
||||
type: string
|
||||
description: 服务区域
|
||||
example: "宁夏回族自治区"
|
||||
add_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 添加时间
|
||||
example: "2024-01-15T10:30:00.000Z"
|
||||
on_sale_status:
|
||||
type: boolean
|
||||
description: 在售状态
|
||||
example: true
|
||||
default: true
|
||||
sort_order:
|
||||
type: integer
|
||||
description: 排序顺序
|
||||
example: 0
|
||||
default: 0
|
||||
remarks:
|
||||
type: string
|
||||
description: 备注
|
||||
example: "特殊说明信息"
|
||||
status:
|
||||
type: string
|
||||
enum: [active, inactive]
|
||||
description: 险种状态
|
||||
example: "active"
|
||||
default: "active"
|
||||
|
||||
UpdateInsuranceTypeRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: 险种名称
|
||||
example: "牛羊保险"
|
||||
minLength: 1
|
||||
maxLength: 100
|
||||
description:
|
||||
type: string
|
||||
description: 险种描述
|
||||
example: "针对牛羊等大型牲畜的综合保险产品"
|
||||
applicable_livestock:
|
||||
type: string
|
||||
description: 适用牲畜类型
|
||||
example: "牛、羊"
|
||||
maxLength: 100
|
||||
insurance_term:
|
||||
type: integer
|
||||
description: 保险期限(月)
|
||||
example: 12
|
||||
minimum: 1
|
||||
policy_form:
|
||||
type: string
|
||||
description: 保单形式
|
||||
example: "电子保单"
|
||||
maxLength: 50
|
||||
coverage_amount_min:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 最低保额
|
||||
example: 1000.00
|
||||
minimum: 0
|
||||
coverage_amount_max:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 最高保额
|
||||
example: 50000.00
|
||||
minimum: 0
|
||||
premium_rate:
|
||||
type: number
|
||||
format: decimal
|
||||
description: 保险费率
|
||||
example: 0.0350
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
service_area:
|
||||
type: string
|
||||
description: 服务区域
|
||||
example: "宁夏回族自治区"
|
||||
add_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 添加时间
|
||||
example: "2024-01-15T10:30:00.000Z"
|
||||
on_sale_status:
|
||||
type: boolean
|
||||
description: 在售状态
|
||||
example: true
|
||||
sort_order:
|
||||
type: integer
|
||||
description: 排序顺序
|
||||
example: 0
|
||||
remarks:
|
||||
type: string
|
||||
description: 备注
|
||||
example: "特殊说明信息"
|
||||
status:
|
||||
type: string
|
||||
enum: [active, inactive]
|
||||
description: 险种状态
|
||||
example: "active"
|
||||
|
||||
UpdateInsuranceTypeStatusRequest:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [active, inactive]
|
||||
description: 险种状态
|
||||
example: "active"
|
||||
on_sale_status:
|
||||
type: boolean
|
||||
description: 在售状态
|
||||
example: true
|
||||
|
||||
Pagination:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
description: 当前页码
|
||||
example: 1
|
||||
limit:
|
||||
type: integer
|
||||
description: 每页数量
|
||||
example: 10
|
||||
total:
|
||||
type: integer
|
||||
description: 总记录数
|
||||
example: 100
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: 请求参数错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
message:
|
||||
type: string
|
||||
example: "请求参数错误"
|
||||
|
||||
Unauthorized:
|
||||
description: 未授权
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
message:
|
||||
type: string
|
||||
example: "未授权访问"
|
||||
|
||||
Forbidden:
|
||||
description: 权限不足
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
message:
|
||||
type: string
|
||||
example: "权限不足"
|
||||
|
||||
NotFound:
|
||||
description: 资源不存在
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
message:
|
||||
type: string
|
||||
example: "险种不存在"
|
||||
|
||||
InternalServerError:
|
||||
description: 服务器内部错误
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: false
|
||||
message:
|
||||
type: string
|
||||
example: "服务器内部错误"
|
||||
@@ -29,6 +29,38 @@ const InsuranceType = sequelize.define('InsuranceType', {
|
||||
allowNull: true,
|
||||
comment: '保险类型描述'
|
||||
},
|
||||
applicable_livestock: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '适用生资(如:牛、羊、猪等)'
|
||||
},
|
||||
insurance_term: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '保险期限(月)',
|
||||
validate: {
|
||||
min: {
|
||||
args: [1],
|
||||
msg: '保险期限不能小于1个月'
|
||||
}
|
||||
}
|
||||
},
|
||||
policy_form: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '保单形式(电子保单、纸质保单等)'
|
||||
},
|
||||
on_sale_status: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '在售状态(true-在售,false-停售)'
|
||||
},
|
||||
add_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '添加时间'
|
||||
},
|
||||
coverage_amount_min: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
|
||||
@@ -19,6 +19,21 @@ const LivestockClaim = sequelize.define('LivestockClaim', {
|
||||
}
|
||||
}
|
||||
},
|
||||
reporter_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '报案人姓名'
|
||||
},
|
||||
contact_phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
comment: '联系电话'
|
||||
},
|
||||
policy_no: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '保单号'
|
||||
},
|
||||
policy_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
@@ -29,9 +44,32 @@ const LivestockClaim = sequelize.define('LivestockClaim', {
|
||||
}
|
||||
},
|
||||
claim_type: {
|
||||
type: DataTypes.ENUM('disease', 'natural_disaster', 'accident', 'theft', 'other'),
|
||||
type: DataTypes.ENUM('death', 'disease', 'accident', 'natural_disaster'),
|
||||
allowNull: false,
|
||||
comment: '理赔类型:disease-疾病,natural_disaster-自然灾害,accident-意外事故,theft-盗窃,other-其他'
|
||||
comment: '理赔类型:death-死亡,disease-疾病,accident-意外事故,natural_disaster-自然灾害'
|
||||
},
|
||||
affected_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '受影响牲畜数量',
|
||||
validate: {
|
||||
min: {
|
||||
args: [1],
|
||||
msg: '受影响牲畜数量不能小于1'
|
||||
}
|
||||
}
|
||||
},
|
||||
claim_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
comment: '理赔金额',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '理赔金额不能小于0'
|
||||
}
|
||||
}
|
||||
},
|
||||
incident_date: {
|
||||
type: DataTypes.DATE,
|
||||
@@ -63,28 +101,6 @@ const LivestockClaim = sequelize.define('LivestockClaim', {
|
||||
}
|
||||
}
|
||||
},
|
||||
affected_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '受影响牲畜数量',
|
||||
validate: {
|
||||
min: {
|
||||
args: [1],
|
||||
msg: '受影响牲畜数量不能小于1'
|
||||
}
|
||||
}
|
||||
},
|
||||
claim_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
comment: '理赔金额',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '理赔金额不能小于0'
|
||||
}
|
||||
}
|
||||
},
|
||||
claim_status: {
|
||||
type: DataTypes.ENUM('pending', 'investigating', 'approved', 'rejected', 'paid'),
|
||||
allowNull: false,
|
||||
@@ -100,15 +116,10 @@ const LivestockClaim = sequelize.define('LivestockClaim', {
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
investigation_notes: {
|
||||
investigation_report: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '调查备注'
|
||||
},
|
||||
investigation_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '调查日期'
|
||||
comment: '调查报告'
|
||||
},
|
||||
reviewer_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
@@ -129,7 +140,12 @@ const LivestockClaim = sequelize.define('LivestockClaim', {
|
||||
allowNull: true,
|
||||
comment: '审核日期'
|
||||
},
|
||||
settlement_amount: {
|
||||
payment_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '赔付日期'
|
||||
},
|
||||
payment_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: true,
|
||||
comment: '实际赔付金额',
|
||||
@@ -140,11 +156,6 @@ const LivestockClaim = sequelize.define('LivestockClaim', {
|
||||
}
|
||||
}
|
||||
},
|
||||
settlement_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '赔付日期'
|
||||
},
|
||||
supporting_documents: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
|
||||
@@ -212,7 +212,7 @@ LivestockClaim.belongsTo(User, {
|
||||
as: 'creator'
|
||||
});
|
||||
LivestockClaim.belongsTo(User, {
|
||||
foreignKey: 'reviewed_by',
|
||||
foreignKey: 'reviewer_id',
|
||||
as: 'reviewer'
|
||||
});
|
||||
|
||||
|
||||
@@ -3,34 +3,328 @@ const router = express.Router();
|
||||
const insuranceTypeController = require('../controllers/insuranceTypeController');
|
||||
const { jwtAuth, checkPermission } = require('../middleware/auth');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/insurance-types:
|
||||
* get:
|
||||
* summary: 获取险种列表
|
||||
* description: 分页获取险种列表,支持多种筛选条件
|
||||
* tags:
|
||||
* - 险种管理
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - name: page
|
||||
* in: query
|
||||
* description: 页码
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* minimum: 1
|
||||
* - name: pageSize
|
||||
* in: query
|
||||
* description: 每页数量
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* - name: name
|
||||
* in: query
|
||||
* description: 险种名称(模糊搜索)
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: status
|
||||
* in: query
|
||||
* description: 险种状态
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [active, inactive]
|
||||
* - name: applicable_livestock
|
||||
* in: query
|
||||
* description: 适用牲畜类型(模糊搜索)
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: service_area
|
||||
* in: query
|
||||
* description: 服务区域(模糊搜索)
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* - name: on_sale_status
|
||||
* in: query
|
||||
* description: 在售状态
|
||||
* required: false
|
||||
* schema:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* '200':
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: "获取险种列表成功"
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/InsuranceType'
|
||||
* pagination:
|
||||
* $ref: '#/components/schemas/Pagination'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/ForbiddenError'
|
||||
* '500':
|
||||
* description: 服务器内部错误
|
||||
* post:
|
||||
* summary: 创建险种
|
||||
* description: 创建新的保险险种
|
||||
* tags:
|
||||
* - 险种管理
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/CreateInsuranceTypeRequest'
|
||||
* responses:
|
||||
* '201':
|
||||
* description: 创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: "创建险种成功"
|
||||
* data:
|
||||
* $ref: '#/components/schemas/InsuranceType'
|
||||
* '400':
|
||||
* description: 请求参数错误
|
||||
* '401':
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/ForbiddenError'
|
||||
* '500':
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
|
||||
// 获取险种列表
|
||||
router.get('/', jwtAuth, checkPermission('insurance_type', 'read'),
|
||||
insuranceTypeController.getInsuranceTypes
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/insurance-types/{id}:
|
||||
* get:
|
||||
* summary: 获取单个险种
|
||||
* description: 根据ID获取险种详细信息
|
||||
* tags:
|
||||
* - 险种管理
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* description: 险种ID
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* '200':
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: "获取险种详情成功"
|
||||
* data:
|
||||
* $ref: '#/components/schemas/InsuranceType'
|
||||
* '401':
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/ForbiddenError'
|
||||
* '404':
|
||||
* description: 险种不存在
|
||||
* '500':
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
// 获取单个险种详情
|
||||
router.get('/:id', jwtAuth, checkPermission('insurance_type', 'read'),
|
||||
insuranceTypeController.getInsuranceTypeById
|
||||
);
|
||||
|
||||
// 创建险种
|
||||
router.post('/', jwtAuth, checkPermission('insurance_type', 'create'),
|
||||
insuranceTypeController.createInsuranceType
|
||||
);
|
||||
router.post('/', jwtAuth, checkPermission('insurance_type', 'create'), insuranceTypeController.createInsuranceType);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/insurance-types/{id}:
|
||||
* put:
|
||||
* summary: 更新险种
|
||||
* description: 根据ID更新险种信息
|
||||
* tags:
|
||||
* - 险种管理
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* description: 险种ID
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/UpdateInsuranceTypeRequest'
|
||||
* responses:
|
||||
* '200':
|
||||
* description: 更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: "更新险种成功"
|
||||
* data:
|
||||
* $ref: '#/components/schemas/InsuranceType'
|
||||
* '400':
|
||||
* description: 请求参数错误
|
||||
* '401':
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/ForbiddenError'
|
||||
* '404':
|
||||
* description: 险种不存在
|
||||
* '500':
|
||||
* description: 服务器内部错误
|
||||
* delete:
|
||||
* summary: 删除险种
|
||||
* description: 根据ID删除险种
|
||||
* tags:
|
||||
* - 险种管理
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* description: 险种ID
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* '200':
|
||||
* description: 删除成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: "删除险种成功"
|
||||
* '401':
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/ForbiddenError'
|
||||
* '404':
|
||||
* description: 险种不存在
|
||||
* '500':
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
// 更新险种
|
||||
router.put('/:id', jwtAuth, checkPermission('insurance_type', 'update'),
|
||||
insuranceTypeController.updateInsuranceType
|
||||
);
|
||||
router.put('/:id', jwtAuth, checkPermission('insurance_type', 'update'), insuranceTypeController.updateInsuranceType);
|
||||
|
||||
// 删除险种
|
||||
router.delete('/:id', jwtAuth, checkPermission('insurance_type', 'delete'),
|
||||
insuranceTypeController.deleteInsuranceType
|
||||
);
|
||||
router.delete('/:id', jwtAuth, checkPermission('insurance_type', 'delete'), insuranceTypeController.deleteInsuranceType);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/insurance-types/{id}/status:
|
||||
* patch:
|
||||
* summary: 更新险种状态
|
||||
* description: 更新险种的状态和在售状态
|
||||
* tags:
|
||||
* - 险种管理
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* description: 险种ID
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/UpdateInsuranceTypeStatusRequest'
|
||||
* responses:
|
||||
* '200':
|
||||
* description: 更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* example: true
|
||||
* message:
|
||||
* type: string
|
||||
* example: "更新险种状态成功"
|
||||
* data:
|
||||
* $ref: '#/components/schemas/InsuranceType'
|
||||
* '400':
|
||||
* description: 请求参数错误
|
||||
* '401':
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* '403':
|
||||
* $ref: '#/components/responses/ForbiddenError'
|
||||
* '404':
|
||||
* description: 险种不存在
|
||||
* '500':
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
// 更新险种状态
|
||||
router.patch('/:id/status', jwtAuth, checkPermission('insurance_type', 'update'),
|
||||
insuranceTypeController.updateInsuranceTypeStatus
|
||||
);
|
||||
router.patch('/:id/status', jwtAuth, checkPermission('insurance_type', 'update'), insuranceTypeController.updateInsuranceTypeStatus);
|
||||
|
||||
module.exports = router;
|
||||
@@ -6,10 +6,27 @@ const {
|
||||
getLivestockClaimById,
|
||||
reviewLivestockClaim,
|
||||
updateLivestockClaimPayment,
|
||||
getLivestockClaimStats
|
||||
getLivestockClaimStats,
|
||||
updateLivestockClaim,
|
||||
deleteLivestockClaim
|
||||
} = require('../controllers/livestockClaimController');
|
||||
const { authenticateToken, requirePermission } = require('../middleware/auth');
|
||||
|
||||
// 测试查询参数端点(无认证)
|
||||
router.get('/test-params', (req, res) => {
|
||||
console.log('🧪 [测试端点] 查询参数测试:');
|
||||
console.log('req.url:', req.url);
|
||||
console.log('req.originalUrl:', req.originalUrl);
|
||||
console.log('req.query:', req.query);
|
||||
console.log('req.params:', req.params);
|
||||
res.json({
|
||||
url: req.url,
|
||||
originalUrl: req.originalUrl,
|
||||
query: req.query,
|
||||
params: req.params
|
||||
});
|
||||
});
|
||||
|
||||
// 获取生资理赔列表
|
||||
router.get('/', authenticateToken, requirePermission('livestock_claim:read'), getLivestockClaims);
|
||||
|
||||
@@ -22,6 +39,12 @@ router.get('/:id', authenticateToken, requirePermission('livestock_claim:read'),
|
||||
// 创建生资理赔申请
|
||||
router.post('/', authenticateToken, requirePermission('livestock_claim:create'), createLivestockClaim);
|
||||
|
||||
// 更新生资理赔信息
|
||||
router.put('/:id', authenticateToken, requirePermission('livestock_claim:update'), updateLivestockClaim);
|
||||
|
||||
// 删除生资理赔
|
||||
router.delete('/:id', authenticateToken, requirePermission('livestock_claim:delete'), deleteLivestockClaim);
|
||||
|
||||
// 审核生资理赔
|
||||
router.patch('/:id/review', authenticateToken, requirePermission('livestock_claim:review'), reviewLivestockClaim);
|
||||
|
||||
|
||||
23
insurance_backend/test_params.js
Normal file
23
insurance_backend/test_params.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const axios = require('axios');
|
||||
|
||||
async function testParams() {
|
||||
try {
|
||||
console.log('🧪 测试查询参数传递...');
|
||||
|
||||
const response = await axios.get('http://localhost:3000/api/livestock-claims/test-params', {
|
||||
params: {
|
||||
claim_no: 'LC2024001',
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ 测试端点响应:');
|
||||
console.log(JSON.stringify(response.data, null, 2));
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error.response?.data || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testParams();
|
||||
282
insurance_backend/tests/insurance-type-api-test.js
Normal file
282
insurance_backend/tests/insurance-type-api-test.js
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 险种管理API测试脚本
|
||||
* 使用Node.js和axios进行API测试
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
// 配置
|
||||
const BASE_URL = 'http://localhost:3000/api';
|
||||
const TEST_USER = {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
};
|
||||
|
||||
let authToken = '';
|
||||
let testInsuranceTypeId = null;
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// 请求拦截器 - 添加认证头
|
||||
api.interceptors.request.use(config => {
|
||||
if (authToken) {
|
||||
config.headers.Authorization = `Bearer ${authToken}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// 响应拦截器 - 处理错误
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
console.error('API请求错误:', error.response?.data || error.message);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 测试函数
|
||||
async function runTests() {
|
||||
console.log('🚀 开始险种管理API测试...\n');
|
||||
|
||||
try {
|
||||
// 1. 用户登录
|
||||
await testLogin();
|
||||
|
||||
// 2. 获取险种列表
|
||||
await testGetInsuranceTypes();
|
||||
|
||||
// 3. 创建险种
|
||||
await testCreateInsuranceType();
|
||||
|
||||
// 4. 获取单个险种
|
||||
await testGetInsuranceTypeById();
|
||||
|
||||
// 5. 更新险种
|
||||
await testUpdateInsuranceType();
|
||||
|
||||
// 6. 更新险种状态
|
||||
await testUpdateInsuranceTypeStatus();
|
||||
|
||||
// 7. 删除险种
|
||||
await testDeleteInsuranceType();
|
||||
|
||||
console.log('✅ 所有测试完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 测试用户登录
|
||||
async function testLogin() {
|
||||
console.log('📝 测试用户登录...');
|
||||
|
||||
try {
|
||||
const response = await api.post('/auth/login', TEST_USER);
|
||||
|
||||
if (response.data.success && response.data.data.token) {
|
||||
authToken = response.data.data.token;
|
||||
console.log('✅ 登录成功');
|
||||
} else {
|
||||
throw new Error('登录失败:未获取到token');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`登录失败: ${error.response?.data?.message || error.message}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 2. 测试获取险种列表
|
||||
async function testGetInsuranceTypes() {
|
||||
console.log('📋 测试获取险种列表...');
|
||||
|
||||
try {
|
||||
const response = await api.get('/insurance-types', {
|
||||
params: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
status: 'active'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log(`✅ 获取险种列表成功,共 ${response.data.pagination.total} 条记录`);
|
||||
console.log(` 当前页: ${response.data.pagination.page}/${response.data.pagination.totalPages}`);
|
||||
} else {
|
||||
throw new Error('获取险种列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`获取险种列表失败: ${error.response?.data?.message || error.message}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 3. 测试创建险种
|
||||
async function testCreateInsuranceType() {
|
||||
console.log('➕ 测试创建险种...');
|
||||
|
||||
const testData = {
|
||||
name: `测试险种_${Date.now()}`,
|
||||
description: '这是一个测试用的险种',
|
||||
applicable_livestock: '牛',
|
||||
insurance_term: '1年',
|
||||
policy_form: '团体保险',
|
||||
coverage_amount_min: 1000.00,
|
||||
coverage_amount_max: 50000.00,
|
||||
premium_rate: 0.05
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await api.post('/insurance-types', testData);
|
||||
|
||||
if (response.data.success && response.data.data.id) {
|
||||
testInsuranceTypeId = response.data.data.id;
|
||||
console.log(`✅ 创建险种成功,ID: ${testInsuranceTypeId}`);
|
||||
console.log(` 险种名称: ${response.data.data.name}`);
|
||||
} else {
|
||||
throw new Error('创建险种失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`创建险种失败: ${error.response?.data?.message || error.message}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 4. 测试获取单个险种
|
||||
async function testGetInsuranceTypeById() {
|
||||
console.log('🔍 测试获取单个险种...');
|
||||
|
||||
if (!testInsuranceTypeId) {
|
||||
throw new Error('没有可用的测试险种ID');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get(`/insurance-types/${testInsuranceTypeId}`);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
console.log(`✅ 获取险种详情成功`);
|
||||
console.log(` 险种名称: ${response.data.data.name}`);
|
||||
console.log(` 适用牲畜: ${response.data.data.applicable_livestock}`);
|
||||
console.log(` 保险期限: ${response.data.data.insurance_term}`);
|
||||
} else {
|
||||
throw new Error('获取险种详情失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`获取险种详情失败: ${error.response?.data?.message || error.message}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 5. 测试更新险种
|
||||
async function testUpdateInsuranceType() {
|
||||
console.log('✏️ 测试更新险种...');
|
||||
|
||||
if (!testInsuranceTypeId) {
|
||||
throw new Error('没有可用的测试险种ID');
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
description: '这是一个更新后的测试险种描述',
|
||||
premium_rate: 0.08
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await api.put(`/insurance-types/${testInsuranceTypeId}`, updateData);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
console.log(`✅ 更新险种成功`);
|
||||
console.log(` 新描述: ${response.data.data.description}`);
|
||||
console.log(` 新费率: ${response.data.data.premium_rate}`);
|
||||
} else {
|
||||
throw new Error('更新险种失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`更新险种失败: ${error.response?.data?.message || error.message}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 6. 测试更新险种状态
|
||||
async function testUpdateInsuranceTypeStatus() {
|
||||
console.log('🔄 测试更新险种状态...');
|
||||
|
||||
if (!testInsuranceTypeId) {
|
||||
throw new Error('没有可用的测试险种ID');
|
||||
}
|
||||
|
||||
const statusData = {
|
||||
status: 'active',
|
||||
on_sale_status: true
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await api.patch(`/insurance-types/${testInsuranceTypeId}/status`, statusData);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
console.log(`✅ 更新险种状态成功`);
|
||||
console.log(` 状态: ${response.data.data.status}`);
|
||||
console.log(` 在售状态: ${response.data.data.on_sale_status}`);
|
||||
} else {
|
||||
throw new Error('更新险种状态失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`更新险种状态失败: ${error.response?.data?.message || error.message}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 7. 测试删除险种
|
||||
async function testDeleteInsuranceType() {
|
||||
console.log('🗑️ 测试删除险种...');
|
||||
|
||||
if (!testInsuranceTypeId) {
|
||||
throw new Error('没有可用的测试险种ID');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.delete(`/insurance-types/${testInsuranceTypeId}`);
|
||||
|
||||
if (response.data.success) {
|
||||
console.log(`✅ 删除险种成功,ID: ${testInsuranceTypeId}`);
|
||||
} else {
|
||||
throw new Error('删除险种失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`删除险种失败: ${error.response?.data?.message || error.message}`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
if (require.main === module) {
|
||||
runTests().catch(error => {
|
||||
console.error('测试运行失败:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runTests,
|
||||
testLogin,
|
||||
testGetInsuranceTypes,
|
||||
testCreateInsuranceType,
|
||||
testGetInsuranceTypeById,
|
||||
testUpdateInsuranceType,
|
||||
testUpdateInsuranceTypeStatus,
|
||||
testDeleteInsuranceType
|
||||
};
|
||||
@@ -5,6 +5,10 @@
|
||||
"pages/profile/profile",
|
||||
"pages/login/login",
|
||||
"pages/cattle/cattle",
|
||||
"pages/cattle/transfer/transfer",
|
||||
"pages/cattle/exit/exit",
|
||||
"pages/cattle/pens/pens",
|
||||
"pages/cattle/batches/batches",
|
||||
"pages/device/device",
|
||||
"pages/device/eartag/eartag",
|
||||
"pages/device/collar/collar",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// pages/alert/alert.js
|
||||
const { get, post } = require('../../utils/api')
|
||||
const { formatDate, formatTime } = require('../../utils/index')
|
||||
// 引入预警相关真实接口
|
||||
const { alertApi } = require('../../services/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -14,6 +16,10 @@ Page({
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
// 详情弹窗
|
||||
detailVisible: false,
|
||||
detailData: null,
|
||||
detailPairs: [],
|
||||
alertTypes: [
|
||||
{ value: 'eartag', label: '耳标预警', icon: '🏷️' },
|
||||
{ value: 'collar', label: '项圈预警', icon: '📱' },
|
||||
@@ -28,7 +34,19 @@ Page({
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
onLoad(options) {
|
||||
// 允许通过URL参数设置默认筛选:?type=eartag&status=pending
|
||||
const { type, status } = options || {}
|
||||
const validTypes = this.data.alertTypes.map(t => t.value)
|
||||
const validStatuses = this.data.alertStatuses.map(s => s.value)
|
||||
|
||||
if (type && validTypes.includes(type)) {
|
||||
this.setData({ typeFilter: type })
|
||||
}
|
||||
if (status && validStatuses.includes(status)) {
|
||||
this.setData({ statusFilter: status })
|
||||
}
|
||||
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
@@ -160,34 +178,44 @@ Page({
|
||||
this.loadAlertList()
|
||||
},
|
||||
|
||||
// 查看预警详情
|
||||
viewAlertDetail(e) {
|
||||
// 查看预警详情(在当前页弹窗展示)
|
||||
async viewAlertDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
|
||||
let url = ''
|
||||
switch (type) {
|
||||
case 'eartag':
|
||||
url = `/pages/alert/eartag/eartag?id=${id}`
|
||||
break
|
||||
case 'collar':
|
||||
url = `/pages/alert/collar/collar?id=${id}`
|
||||
break
|
||||
case 'fence':
|
||||
url = `/pages/alert/fence/fence?id=${id}`
|
||||
break
|
||||
case 'health':
|
||||
url = `/pages/alert/health/health?id=${id}`
|
||||
break
|
||||
default:
|
||||
wx.showToast({
|
||||
title: '未知预警类型',
|
||||
icon: 'none'
|
||||
})
|
||||
|
||||
try {
|
||||
wx.showLoading({ title: '加载详情...' })
|
||||
|
||||
let res
|
||||
if (type === 'eartag') {
|
||||
res = await alertApi.getEartagAlertDetail(id)
|
||||
} else if (type === 'collar') {
|
||||
res = await alertApi.getCollarAlertDetail(id)
|
||||
} else {
|
||||
// 其他类型暂未实现,给出提示
|
||||
wx.showToast({ title: '该类型详情暂未实现', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const detail = res?.data || res
|
||||
const detailPairs = this.mapAlertDetail(type, detail)
|
||||
|
||||
this.setData({
|
||||
detailVisible: true,
|
||||
detailData: detail,
|
||||
detailPairs
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('加载预警详情失败:', err)
|
||||
wx.showToast({ title: '加载详情失败', icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
|
||||
wx.navigateTo({ url })
|
||||
},
|
||||
|
||||
// 关闭详情弹窗
|
||||
closeDetail() {
|
||||
this.setData({ detailVisible: false, detailData: null, detailPairs: [] })
|
||||
},
|
||||
|
||||
// 处理预警
|
||||
@@ -386,6 +414,61 @@ Page({
|
||||
return colorMap[priority] || '#909399'
|
||||
},
|
||||
|
||||
// 映射预警详情字段为键值对用于展示
|
||||
mapAlertDetail(type, d) {
|
||||
if (!d) return []
|
||||
|
||||
// 通用映射函数
|
||||
const levelText = {
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
urgent: '紧急'
|
||||
}
|
||||
const typeText = {
|
||||
temperature: '温度',
|
||||
battery: '电量',
|
||||
movement: '运动',
|
||||
offline: '离线',
|
||||
gps: '定位',
|
||||
eartag: '耳标',
|
||||
collar: '项圈'
|
||||
}
|
||||
|
||||
const pairs = []
|
||||
// 基本信息
|
||||
pairs.push({ label: '预警ID', value: d.id || '-' })
|
||||
pairs.push({ label: '异常类型', value: typeText[d.alertType] || (d.alertType || '-') })
|
||||
pairs.push({ label: '异常等级', value: levelText[d.alertLevel] || (d.alertLevel || '-') })
|
||||
pairs.push({ label: '预警时间', value: d.alertTime ? this.formatTime(d.alertTime) : '-' })
|
||||
|
||||
if (type === 'eartag') {
|
||||
pairs.push({ label: '耳标编号', value: d.eartagNumber || d.deviceName || '-' })
|
||||
pairs.push({ label: '设备ID', value: d.deviceId || '-' })
|
||||
pairs.push({ label: '设备状态', value: d.deviceStatus || '-' })
|
||||
// 传感数据
|
||||
if (d.temperature !== undefined) pairs.push({ label: '温度(°C)', value: d.temperature })
|
||||
if (d.battery !== undefined) pairs.push({ label: '电量(%)', value: d.battery })
|
||||
if (d.gpsSignal) pairs.push({ label: 'GPS信号', value: d.gpsSignal })
|
||||
if (d.longitude !== undefined) pairs.push({ label: '经度', value: d.longitude })
|
||||
if (d.latitude !== undefined) pairs.push({ label: '纬度', value: d.latitude })
|
||||
if (d.movementStatus) pairs.push({ label: '运动状态', value: d.movementStatus })
|
||||
if (d.dailySteps !== undefined) pairs.push({ label: '今日步数', value: d.dailySteps })
|
||||
if (d.yesterdaySteps !== undefined) pairs.push({ label: '昨日步数', value: d.yesterdaySteps })
|
||||
if (d.totalSteps !== undefined) pairs.push({ label: '总步数', value: d.totalSteps })
|
||||
if (d.description) pairs.push({ label: '描述', value: d.description })
|
||||
} else if (type === 'collar') {
|
||||
// 可按需要加入项圈详情字段
|
||||
pairs.push({ label: '设备ID', value: d.deviceId || '-' })
|
||||
pairs.push({ label: '设备名称', value: d.deviceName || '-' })
|
||||
if (d.battery !== undefined) pairs.push({ label: '电量(%)', value: d.battery })
|
||||
if (d.temperature !== undefined) pairs.push({ label: '温度(°C)', value: d.temperature })
|
||||
if (d.description) pairs.push({ label: '描述', value: d.description })
|
||||
}
|
||||
|
||||
return pairs
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(date) {
|
||||
return formatDate(date)
|
||||
|
||||
@@ -153,4 +153,24 @@
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<view wx:if="{{detailVisible}}" class="detail-mask" catchtouchmove="true">
|
||||
<view class="detail-panel">
|
||||
<view class="detail-header">
|
||||
<text class="detail-title">预警详情</text>
|
||||
<text class="detail-close" bindtap="closeDetail">✖</text>
|
||||
</view>
|
||||
<view class="detail-body">
|
||||
<view class="detail-row" wx:for="{{detailPairs}}" wx:key="label">
|
||||
<text class="detail-label">{{item.label}}</text>
|
||||
<text class="detail-value">{{item.value}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail-footer">
|
||||
<button class="primary" bindtap="handleAlert" data-id="{{detailData.id}}" data-type="{{typeFilter === 'all' ? (detailData.alertType || 'eartag') : typeFilter}}">处理</button>
|
||||
<button class="plain" bindtap="ignoreAlert" data-id="{{detailData.id}}" data-type="{{typeFilter === 'all' ? (detailData.alertType || 'eartag') : typeFilter}}">忽略</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -248,6 +248,76 @@
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 详情弹窗样式 */
|
||||
.detail-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.45);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
width: 100%;
|
||||
max-height: 70vh;
|
||||
background: #fff;
|
||||
border-top-left-radius: 24rpx;
|
||||
border-top-right-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-title { font-size: 32rpx; font-weight: 600; color: #303133; }
|
||||
.detail-close { font-size: 32rpx; color: #909399; }
|
||||
|
||||
.detail-body {
|
||||
padding: 16rpx 24rpx;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx dashed #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-label { font-size: 26rpx; color: #606266; }
|
||||
.detail-value { font-size: 26rpx; color: #303133; }
|
||||
|
||||
.detail-footer {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-footer .primary {
|
||||
flex: 1;
|
||||
background-color: #3cc51f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.detail-footer .plain {
|
||||
flex: 1;
|
||||
background-color: #f5f5f5;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
// pages/cattle/batches/batches.js
|
||||
const { cattleBatchApi } = require('../../../services/api.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
records: [],
|
||||
search: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 1,
|
||||
pages: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 输入
|
||||
onSearchInput(e) {
|
||||
this.setData({ search: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onSearchConfirm() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 分页按钮
|
||||
prevPage() {
|
||||
if (this.data.page > 1) {
|
||||
this.setData({ page: this.data.page - 1 })
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if (this.data.page < this.data.totalPages) {
|
||||
this.setData({ page: this.data.page + 1 })
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
goToPage(e) {
|
||||
const page = Number(e.currentTarget.dataset.page)
|
||||
if (!isNaN(page) && page >= 1 && page <= this.data.totalPages) {
|
||||
this.setData({ page })
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
// 加载数据
|
||||
async loadData() {
|
||||
const { page, pageSize, search } = this.data
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
// 请求参数,按照需求开启精确匹配
|
||||
const params = {
|
||||
page,
|
||||
pageSize,
|
||||
search: search || '',
|
||||
exactMatch: true,
|
||||
strictMatch: true
|
||||
}
|
||||
|
||||
const res = await cattleBatchApi.getBatches(params)
|
||||
// 统一解析数据结构
|
||||
const { list, total, page: curPage, pageSize: size, totalPages } = this.normalizeResponse(res)
|
||||
|
||||
let safeList = Array.isArray(list) ? list : []
|
||||
// 前端二次严格过滤,保证精确查询(名称或编号一模一样)
|
||||
const kw = (search || '').trim()
|
||||
if (kw) {
|
||||
safeList = safeList.filter(it => {
|
||||
const name = String(it.name || it.batch_name || it.batchName || '').trim()
|
||||
const code = String(it.code || it.batch_number || it.batchNumber || '').trim()
|
||||
return name === kw || code === kw
|
||||
})
|
||||
}
|
||||
const mapped = safeList.map(item => this.mapItem(item))
|
||||
|
||||
const pages = this.buildPages(totalPages, curPage)
|
||||
|
||||
this.setData({
|
||||
records: mapped,
|
||||
total: Number(total) || safeList.length,
|
||||
page: Number(curPage) || page,
|
||||
pageSize: Number(size) || pageSize,
|
||||
totalPages: Number(totalPages) || this.calcTotalPages(Number(total) || safeList.length, Number(size) || pageSize),
|
||||
pages
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取批次列表失败:', error)
|
||||
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 解析后端响应,兼容不同结构
|
||||
normalizeResponse(res) {
|
||||
// 可能的结构:
|
||||
// 1) { success: true, data: { list: [...], pagination: { total, page, pageSize, totalPages } } }
|
||||
// 2) { data: { items: [...], total, page, pageSize, totalPages } }
|
||||
// 3) 直接返回数组或对象
|
||||
let dataObj = res?.data ?? res
|
||||
let list = []
|
||||
let total = 0
|
||||
let page = this.data.page
|
||||
let pageSize = this.data.pageSize
|
||||
let totalPages = 1
|
||||
|
||||
if (Array.isArray(dataObj)) {
|
||||
list = dataObj
|
||||
total = dataObj.length
|
||||
} else if (dataObj && typeof dataObj === 'object') {
|
||||
// 优先 data.list / data.items
|
||||
list = dataObj.list || dataObj.items || []
|
||||
const pag = dataObj.pagination || {}
|
||||
total = dataObj.total ?? pag.total ?? (Array.isArray(list) ? list.length : 0)
|
||||
page = dataObj.page ?? pag.page ?? page
|
||||
pageSize = dataObj.pageSize ?? pag.pageSize ?? pageSize
|
||||
totalPages = dataObj.totalPages ?? pag.totalPages ?? this.calcTotalPages(total, pageSize)
|
||||
|
||||
// 如果整体包了一层 { success: true, data: {...} }
|
||||
if (!Array.isArray(list) && dataObj.data) {
|
||||
const inner = dataObj.data
|
||||
list = inner.list || inner.items || []
|
||||
const pag2 = inner.pagination || {}
|
||||
total = inner.total ?? pag2.total ?? (Array.isArray(list) ? list.length : 0)
|
||||
page = inner.page ?? pag2.page ?? page
|
||||
pageSize = inner.pageSize ?? pag2.pageSize ?? pageSize
|
||||
totalPages = inner.totalPages ?? pag2.totalPages ?? this.calcTotalPages(total, pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
return { list, total, page, pageSize, totalPages }
|
||||
},
|
||||
|
||||
calcTotalPages(total, pageSize) {
|
||||
const t = Number(total) || 0
|
||||
const s = Number(pageSize) || 10
|
||||
return t > 0 ? Math.ceil(t / s) : 1
|
||||
},
|
||||
|
||||
buildPages(totalPages, current) {
|
||||
const tp = Number(totalPages) || 1
|
||||
const cur = Number(current) || 1
|
||||
const arr = []
|
||||
for (let i = 1; i <= tp; i++) {
|
||||
arr.push({ num: i, current: i === cur })
|
||||
}
|
||||
return arr
|
||||
},
|
||||
|
||||
// 字段中文映射与格式化
|
||||
mapItem(item) {
|
||||
const keyMap = {
|
||||
id: '批次ID',
|
||||
batch_id: '批次ID',
|
||||
batchId: '批次ID',
|
||||
name: '批次名称',
|
||||
batch_name: '批次名称',
|
||||
batchName: '批次名称',
|
||||
code: '批次编号',
|
||||
batch_number: '批次编号',
|
||||
batchNumber: '批次编号',
|
||||
type: '类型',
|
||||
status: '状态',
|
||||
enabled: '是否启用',
|
||||
currentCount: '当前数量',
|
||||
targetCount: '目标数量',
|
||||
capacity: '容量',
|
||||
remark: '备注',
|
||||
description: '描述',
|
||||
manager: '负责人',
|
||||
farm_id: '养殖场ID',
|
||||
farmId: '养殖场ID',
|
||||
farmName: '养殖场',
|
||||
farm: '养殖场对象',
|
||||
created_at: '创建时间',
|
||||
updated_at: '更新时间',
|
||||
create_time: '创建时间',
|
||||
update_time: '更新时间',
|
||||
createdAt: '创建时间',
|
||||
updatedAt: '更新时间',
|
||||
start_date: '开始日期',
|
||||
end_date: '结束日期',
|
||||
startDate: '开始日期',
|
||||
expectedEndDate: '预计结束日期',
|
||||
actualEndDate: '实际结束日期'
|
||||
}
|
||||
|
||||
// 头部字段
|
||||
const statusStr = this.formatStatus(item.status, item.enabled)
|
||||
const createdAtStr = this.pickTime(item, ['created_at', 'createdAt', 'create_time', 'createTime'])
|
||||
|
||||
// 规范化派生字段(farm对象拆解)
|
||||
const extra = {}
|
||||
if (item && typeof item.farm === 'object' && item.farm) {
|
||||
if (!item.farmName && item.farm.name) extra.farmName = item.farm.name
|
||||
if (!item.farmId && (item.farm.id || item.farm_id)) extra.farmId = item.farm.id || item.farm_id
|
||||
}
|
||||
const merged = { ...item, ...extra }
|
||||
|
||||
// 全量字段展示
|
||||
const pairs = Object.keys(merged || {}).map(k => {
|
||||
const zh = keyMap[k] || k
|
||||
const val = this.formatValue(k, merged[k])
|
||||
return { key: k, keyZh: zh, val }
|
||||
})
|
||||
|
||||
return {
|
||||
...merged,
|
||||
statusStr,
|
||||
createdAtStr,
|
||||
displayPairs: pairs
|
||||
}
|
||||
},
|
||||
|
||||
formatStatus(status, enabled) {
|
||||
if (enabled === true || enabled === 1) return '启用'
|
||||
if (enabled === false || enabled === 0) return '停用'
|
||||
if (status === 'enabled') return '启用'
|
||||
if (status === 'disabled') return '停用'
|
||||
if (status === 'active') return '活动'
|
||||
if (status === 'inactive') return '非活动'
|
||||
if (status === undefined || status === null) return ''
|
||||
return String(status)
|
||||
},
|
||||
|
||||
pickTime(obj, keys) {
|
||||
for (const k of keys) {
|
||||
if (obj && obj[k]) return this.formatDate(obj[k])
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
formatValue(key, val) {
|
||||
if (val === null || val === undefined) return ''
|
||||
// 时间类字段统一格式
|
||||
if (/time|date/i.test(key)) {
|
||||
return this.formatDate(val)
|
||||
}
|
||||
if (typeof val === 'number') return String(val)
|
||||
if (typeof val === 'boolean') return val ? '是' : '否'
|
||||
if (Array.isArray(val)) return val.map(v => typeof v === 'object' ? JSON.stringify(v) : String(v)).join(', ')
|
||||
if (typeof val === 'object') return JSON.stringify(val)
|
||||
return String(val)
|
||||
},
|
||||
|
||||
formatDate(input) {
|
||||
try {
|
||||
const d = new Date(input)
|
||||
if (isNaN(d.getTime())) return String(input)
|
||||
const pad = (n) => (n < 10 ? '0' + n : '' + n)
|
||||
const Y = d.getFullYear()
|
||||
const M = pad(d.getMonth() + 1)
|
||||
const D = pad(d.getDate())
|
||||
const h = pad(d.getHours())
|
||||
const m = pad(d.getMinutes())
|
||||
return `${Y}-${M}-${D} ${h}:${m}`
|
||||
} catch (e) {
|
||||
return String(input)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "牛只批次设置",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<!-- 牛只批次设置页面 -->
|
||||
<view class="page-container">
|
||||
<!-- 顶部搜索区域 -->
|
||||
<view class="search-bar">
|
||||
<input class="search-input" placeholder="请输入批次名称或编号(精确匹配)" value="{{search}}" bindinput="onSearchInput" confirm-type="search" bindconfirm="onSearchConfirm" />
|
||||
<button class="search-btn" bindtap="onSearch">查询</button>
|
||||
</view>
|
||||
|
||||
<!-- 统计与分页信息 -->
|
||||
<view class="summary-bar">
|
||||
<text>总数:{{total}}</text>
|
||||
<text>第 {{page}} / {{totalPages}} 页</text>
|
||||
</view>
|
||||
|
||||
<!-- 批次列表 -->
|
||||
<scroll-view scroll-y class="list-scroll">
|
||||
<block wx:if="{{loading}}">
|
||||
<view class="loading">加载中...</view>
|
||||
</block>
|
||||
|
||||
<block wx:if="{{!loading && records.length === 0}}">
|
||||
<view class="empty">暂无数据</view>
|
||||
</block>
|
||||
|
||||
<block wx:for="{{records}}" wx:key="id">
|
||||
<view class="record-card">
|
||||
<!-- 头部主信息 -->
|
||||
<view class="record-header">
|
||||
<view class="title-line">
|
||||
<text class="name">{{item.name || item.batch_name || item.batchName || '-'}}</text>
|
||||
<text class="code">编号:{{item.code || item.batch_number || item.batchNumber || '-'}}</text>
|
||||
</view>
|
||||
<view class="meta-line">
|
||||
<text class="status" wx:if="{{item.statusStr}}">状态:{{item.statusStr}}</text>
|
||||
<text class="time" wx:if="{{item.createdAtStr}}">创建时间:{{item.createdAtStr}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详情字段(显示全部返回字段,中文映射) -->
|
||||
<view class="record-body">
|
||||
<block wx:for="{{item.displayPairs}}" wx:key="key" wx:for-item="pair">
|
||||
<view class="pair-row">
|
||||
<text class="pair-key">{{pair.keyZh}}</text>
|
||||
<text class="pair-val">{{pair.val}}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="pagination">
|
||||
<button class="page-btn" bindtap="prevPage" disabled="{{page <= 1}}">上一页</button>
|
||||
<scroll-view scroll-x class="page-numbers">
|
||||
<view class="page-items">
|
||||
<block wx:for="{{pages}}" wx:key="index">
|
||||
<view class="page-item {{current ? 'active' : ''}}" data-page="{{num}}" bindtap="goToPage">
|
||||
<text>{{num}}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<button class="page-btn" bindtap="nextPage" disabled="{{page >= totalPages}}">下一页</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,119 @@
|
||||
/* 页面容器 */
|
||||
.page-container {
|
||||
padding: 12rpx 16rpx;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 12rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.search-btn {
|
||||
height: 64rpx;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
/* 概览栏 */
|
||||
.summary-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
/* 列表滚动区 */
|
||||
.list-scroll {
|
||||
max-height: calc(100vh - 280rpx);
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
/* 卡片 */
|
||||
.record-card {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0,0,0,0.06);
|
||||
}
|
||||
.record-header .title-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.record-header .name {
|
||||
font-weight: 600;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.record-header .code {
|
||||
color: #333;
|
||||
}
|
||||
.record-header .meta-line {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
color: #666;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.record-body .pair-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 0;
|
||||
border-bottom: 1px dashed #eee;
|
||||
}
|
||||
.pair-key {
|
||||
color: #888;
|
||||
}
|
||||
.pair-val {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12rpx;
|
||||
background: #fff;
|
||||
padding: 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.page-numbers {
|
||||
width: 60%;
|
||||
}
|
||||
.page-items {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.page-item {
|
||||
min-width: 56rpx;
|
||||
height: 56rpx;
|
||||
line-height: 56rpx;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 12rpx;
|
||||
color: #333;
|
||||
}
|
||||
.page-item.active {
|
||||
background: #3cc51f;
|
||||
color: #fff;
|
||||
border-color: #3cc51f;
|
||||
}
|
||||
.page-btn[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// pages/cattle/cattle.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { get, del } = require('../../utils/api')
|
||||
const { formatDate, formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
@@ -8,11 +8,17 @@ Page({
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
searchKeyword: '',
|
||||
// 设备编号精确查询
|
||||
deviceNumber: '',
|
||||
statusFilter: 'all',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
// 按需求使用每页10条
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
total: 0
|
||||
total: 0,
|
||||
// 分页页码集合
|
||||
pages: [],
|
||||
lastPage: 1
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -51,32 +57,56 @@ Page({
|
||||
|
||||
try {
|
||||
const params = {
|
||||
// 页码与每页条数的常见别名,提升与后端的兼容性
|
||||
page: this.data.page,
|
||||
pageNo: this.data.page,
|
||||
pageIndex: this.data.page,
|
||||
current: this.data.page,
|
||||
pageSize: this.data.pageSize,
|
||||
size: this.data.pageSize,
|
||||
limit: this.data.pageSize,
|
||||
status: this.data.statusFilter === 'all' ? '' : this.data.statusFilter
|
||||
}
|
||||
|
||||
if (this.data.searchKeyword) {
|
||||
params.search = this.data.searchKeyword
|
||||
}
|
||||
// 设备编号精确查询参数(尽可能兼容后端不同命名)
|
||||
if (this.data.deviceNumber) {
|
||||
params.deviceNumber = this.data.deviceNumber
|
||||
params.deviceSn = this.data.deviceNumber
|
||||
params.deviceId = this.data.deviceNumber
|
||||
// 部分接口可能支持exact开关
|
||||
params.exact = true
|
||||
}
|
||||
|
||||
const response = await get('/iot-cattle/public', params)
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data.list || []
|
||||
const cattleList = this.data.page === 1 ? newList : [...this.data.cattleList, ...newList]
|
||||
|
||||
this.setData({
|
||||
cattleList,
|
||||
total: response.data.total || 0,
|
||||
hasMore: cattleList.length < (response.data.total || 0)
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
// 统一兼容响应结构(尽可能兼容不同后端命名)
|
||||
const list = (response && response.data && (response.data.list || response.data.records || response.data.items))
|
||||
|| (response && (response.list || response.records || response.items))
|
||||
|| []
|
||||
const totalRaw = (response && response.data && (response.data.total ?? response.data.totalCount ?? response.data.count))
|
||||
|| (response && (response.total ?? response.totalCount ?? response.count))
|
||||
|| (response && response.page && response.page.total)
|
||||
|| 0
|
||||
const totalPagesOverride = (response && response.data && (response.data.totalPages ?? response.data.pageCount))
|
||||
|| (response && (response.totalPages ?? response.pageCount))
|
||||
|| (response && response.page && (response.page.totalPages ?? response.page.pageCount))
|
||||
|| 0
|
||||
const total = Number(totalRaw) || 0
|
||||
const isUnknownTotal = !(Number(totalPagesOverride) > 0) && !(Number(totalRaw) > 0)
|
||||
console.log('牛只档案接口原始响应:', response)
|
||||
const mappedList = list.map(this.mapCattleRecord)
|
||||
console.log('牛只档案字段映射结果(当前页):', mappedList)
|
||||
const cattleList = this.data.page === 1 ? mappedList : [...this.data.cattleList, ...mappedList]
|
||||
// 根据总页数判断是否还有更多(兼容后端直接返回totalPages/pageCount)。
|
||||
// 当后端未返回 total/totalPages 时,使用“本页数据条数 == pageSize”作为是否还有更多的兜底策略。
|
||||
const totalPages = Math.max(1, Number(totalPagesOverride) || Math.ceil(total / this.data.pageSize))
|
||||
const hasMore = isUnknownTotal ? (mappedList.length >= this.data.pageSize) : (this.data.page < totalPages)
|
||||
this.setData({ cattleList, total, hasMore })
|
||||
console.log('分页计算:', { total, pageSize: this.data.pageSize, totalPages, currentPage: this.data.page, isUnknownTotal })
|
||||
// 生成分页页码
|
||||
this.buildPagination(total, totalPages, isUnknownTotal)
|
||||
} catch (error) {
|
||||
console.error('获取牛只列表失败:', error)
|
||||
wx.showToast({
|
||||
@@ -106,6 +136,13 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 设备编号输入(精确查询)
|
||||
onDeviceInput(e) {
|
||||
this.setData({
|
||||
deviceNumber: e.detail.value.trim()
|
||||
})
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
this.setData({
|
||||
@@ -116,10 +153,27 @@ Page({
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
// 执行设备编号精确查询
|
||||
onDeviceSearch() {
|
||||
if (!this.data.deviceNumber) {
|
||||
wx.showToast({ title: '请输入设备编号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
cattleList: [],
|
||||
// 精确查询时不使用模糊关键词
|
||||
searchKeyword: ''
|
||||
})
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
onClearSearch() {
|
||||
this.setData({
|
||||
searchKeyword: '',
|
||||
deviceNumber: '',
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
cattleList: []
|
||||
@@ -245,5 +299,123 @@ Page({
|
||||
// 格式化时间
|
||||
formatTime(time) {
|
||||
return formatTime(time)
|
||||
},
|
||||
|
||||
// 字段中文映射与安全处理
|
||||
mapCattleRecord(item = {}) {
|
||||
// 统一时间戳(后端可能返回秒级或毫秒级)
|
||||
const normalizeTs = (ts) => {
|
||||
if (ts === null || ts === undefined || ts === '') return ''
|
||||
if (typeof ts === 'number') {
|
||||
return ts < 1000000000000 ? ts * 1000 : ts
|
||||
}
|
||||
// 字符串数字
|
||||
const n = Number(ts)
|
||||
if (!Number.isNaN(n)) return n < 1000000000000 ? n * 1000 : n
|
||||
return ts
|
||||
}
|
||||
|
||||
// 性别映射(仅做友好展示,保留原值)
|
||||
const rawSex = item.sex ?? item.gender
|
||||
const sexText = rawSex === 1 || rawSex === '1' ? '公' : (rawSex === 2 || rawSex === '2' ? '母' : (rawSex ?? '-'))
|
||||
|
||||
return {
|
||||
// 基本标识
|
||||
id: item.id ?? item.cattleId ?? item._id ?? '',
|
||||
name: item.name ?? item.cattleName ?? '',
|
||||
earNumber: item.earNumber ?? item.earNo ?? item.earTag ?? '-',
|
||||
|
||||
// 基本属性
|
||||
breed: item.breed ?? item.breedName ?? item.varieties ?? '-',
|
||||
strain: item.strain ?? '-',
|
||||
varieties: item.varieties ?? '-',
|
||||
cate: item.cate ?? '-',
|
||||
gender: rawSex ?? '-',
|
||||
genderText: sexText,
|
||||
age: item.age ?? item.ageYear ?? '-',
|
||||
ageInMonths: item.ageInMonths ?? item.ageMonth ?? '-',
|
||||
|
||||
// 体重与计算
|
||||
weight: item.weight ?? item.currentWeight ?? '-',
|
||||
currentWeight: item.currentWeight ?? '-',
|
||||
birthWeight: item.birthWeight ?? '-',
|
||||
sourceWeight: item.sourceWeight ?? '-',
|
||||
weightCalculateTime: item.weightCalculateTime ? formatDate(normalizeTs(item.weightCalculateTime), 'YYYY-MM-DD HH:mm:ss') : '',
|
||||
|
||||
// 来源信息
|
||||
source: item.source ?? '-',
|
||||
sourceDay: item.sourceDay ?? '-',
|
||||
|
||||
// 关联位置与组织
|
||||
deviceNumber: item.deviceNumber ?? item.deviceSn ?? item.deviceId ?? '-',
|
||||
penId: item.penId ?? '-',
|
||||
penName: item.penName ?? item.barnName ?? '-',
|
||||
batchId: item.batchId ?? '-',
|
||||
batchName: item.batchName ?? '-',
|
||||
farmId: item.farmId ?? '-',
|
||||
farmName: item.farmName ?? '-',
|
||||
|
||||
// 生育与阶段
|
||||
parity: item.parity ?? '-',
|
||||
physiologicalStage: item.physiologicalStage ?? '-',
|
||||
status: item.status ?? item.cattleStatus ?? 'normal',
|
||||
|
||||
// 重要日期
|
||||
birthday: item.birthday ?? item.birthDate ?? item.bornDate ?? item.birthTime ?? '',
|
||||
birthdayStr: item.birthday || item.birthDate || item.bornDate || item.birthTime
|
||||
? formatDate(normalizeTs(item.birthday ?? item.birthDate ?? item.bornDate ?? item.birthTime))
|
||||
: '',
|
||||
dayOfBirthday: item.dayOfBirthday ?? '-',
|
||||
intoTime: item.intoTime ?? '',
|
||||
intoTimeStr: item.intoTime ? formatDate(normalizeTs(item.intoTime)) : ''
|
||||
}
|
||||
},
|
||||
|
||||
// 生成分页页码,控制展示范围并高亮当前页
|
||||
buildPagination(total, totalPagesOverride = 0, isUnknownTotal = false) {
|
||||
const pageSize = this.data.pageSize
|
||||
const current = this.data.page
|
||||
// 当总数未知时,按已加载的当前页生成页码(1..current),允许继续“下一页”。
|
||||
if (isUnknownTotal) {
|
||||
const pages = Array.from({ length: Math.max(1, current) }, (_, i) => i + 1)
|
||||
this.setData({ pages, lastPage: current })
|
||||
return
|
||||
}
|
||||
const totalPages = Math.max(1, Number(totalPagesOverride) || Math.ceil(total / pageSize))
|
||||
let pages = []
|
||||
const maxVisible = 9
|
||||
if (totalPages <= maxVisible) {
|
||||
pages = Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
} else {
|
||||
// 滑动窗口
|
||||
let start = Math.max(1, current - 4)
|
||||
let end = Math.min(totalPages, start + maxVisible - 1)
|
||||
// 保证区间长度
|
||||
start = Math.max(1, end - maxVisible + 1)
|
||||
pages = Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
}
|
||||
this.setData({ pages, lastPage: totalPages })
|
||||
},
|
||||
|
||||
// 上一页
|
||||
onPrevPage() {
|
||||
if (this.data.page <= 1) return
|
||||
this.setData({ page: this.data.page - 1, cattleList: [] })
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
// 下一页
|
||||
onNextPage() {
|
||||
if (!this.data.hasMore) return
|
||||
this.setData({ page: this.data.page + 1, cattleList: [] })
|
||||
this.loadCattleList()
|
||||
},
|
||||
|
||||
// 切换页码
|
||||
onPageTap(e) {
|
||||
const targetPage = Number(e.currentTarget.dataset.page)
|
||||
if (!targetPage || targetPage === this.data.page) return
|
||||
this.setData({ page: targetPage, cattleList: [] })
|
||||
this.loadCattleList()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
<view class="search-input-wrapper">
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索牛只耳号、姓名..."
|
||||
placeholder="搜索牛只耳号"
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
bindconfirm="onSearch"
|
||||
/>
|
||||
<text class="search-icon" bindtap="onSearch">🔍</text>
|
||||
</view>
|
||||
|
||||
<text wx:if="{{searchKeyword}}" class="clear-btn" bindtap="onClearSearch">清空</text>
|
||||
</view>
|
||||
|
||||
@@ -73,9 +74,27 @@
|
||||
<text class="detail-item">耳号: {{item.earNumber}}</text>
|
||||
<text class="detail-item">品种: {{item.breed || '未知'}}</text>
|
||||
</view>
|
||||
<view class="cattle-meta">
|
||||
<text class="meta-item">年龄: {{item.age || '未知'}}岁</text>
|
||||
<text class="meta-item">体重: {{item.weight || '未知'}}kg</text>
|
||||
|
||||
<!-- 扩展详情,展示全部字段 -->
|
||||
<view class="cattle-extra">
|
||||
<view class="extra-row"><text class="extra-label">性别:</text><text class="extra-value">{{item.genderText}}</text></view>
|
||||
<view class="extra-row"><text class="extra-label">血统:</text><text class="extra-value">{{item.strain || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">品系:</text><text class="extra-value">{{item.varieties || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">品类:</text><text class="extra-value">{{item.cate || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">年龄(月):</text><text class="extra-value">{{item.ageInMonths || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">出生日期:</text><text class="extra-value">{{item.birthdayStr || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">出生体重(kg):</text><text class="extra-value">{{item.birthWeight || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">当前体重(kg):</text><text class="extra-value">{{item.currentWeight || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">来源:</text><text class="extra-value">{{item.source || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">来源天数:</text><text class="extra-value">{{item.sourceDay || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">来源体重(kg):</text><text class="extra-value">{{item.sourceWeight || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">批次:</text><text class="extra-value">{{item.batchName || '-'}}(ID:{{item.batchId || '-'}})</text></view>
|
||||
<view class="extra-row"><text class="extra-label">农场:</text><text class="extra-value">{{item.farmName || '-'}}(ID:{{item.farmId || '-'}})</text></view>
|
||||
<view class="extra-row"><text class="extra-label">栏舍:</text><text class="extra-value">{{item.penName || '-'}}(ID:{{item.penId || '-'}})</text></view>
|
||||
<view class="extra-row"><text class="extra-label">进场日期:</text><text class="extra-value">{{item.intoTimeStr || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">胎次:</text><text class="extra-value">{{item.parity || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">生理阶段:</text><text class="extra-value">{{item.physiologicalStage || '-'}} </text></view>
|
||||
<view class="extra-row"><text class="extra-label">体重计算时间:</text><text class="extra-value">{{item.weightCalculateTime || '-'}} </text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -112,6 +131,21 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页导航 -->
|
||||
<view wx:if="{{cattleList.length > 0}}" class="pagination">
|
||||
<view class="page-item {{page <= 1 ? 'disabled' : ''}}" bindtap="onPrevPage">上一页</view>
|
||||
<view class="page-item" data-page="1" bindtap="onPageTap">首页</view>
|
||||
<view
|
||||
wx:for="{{pages}}"
|
||||
wx:key="*this"
|
||||
class="page-item {{item === page ? 'active' : ''}}"
|
||||
data-page="{{item}}"
|
||||
bindtap="onPageTap"
|
||||
>{{item}}</view>
|
||||
<view class="page-item" data-page="{{lastPage}}" bindtap="onPageTap">末页</view>
|
||||
<view class="page-item {{page >= lastPage ? 'disabled' : ''}}" bindtap="onNextPage">下一页</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<view class="fab" bindtap="addCattle">
|
||||
<text class="fab-icon">+</text>
|
||||
|
||||
@@ -139,6 +139,26 @@
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
|
||||
/* 扩展详情样式 */
|
||||
.cattle-extra {
|
||||
margin-top: 8rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6rpx 12rpx;
|
||||
}
|
||||
.extra-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.extra-label {
|
||||
color: #909399;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
.extra-value {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.cattle-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -223,6 +243,37 @@
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
/* 分页导航 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 16rpx 40rpx;
|
||||
}
|
||||
|
||||
.page-item {
|
||||
min-width: 60rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: #f5f5f5;
|
||||
color: #606266;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-item.active {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 分页禁用态 */
|
||||
.page-item.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
|
||||
258
mini_program/farm-monitor-dashboard/pages/cattle/exit/exit.js
Normal file
258
mini_program/farm-monitor-dashboard/pages/cattle/exit/exit.js
Normal file
@@ -0,0 +1,258 @@
|
||||
// pages/cattle/exit/exit.js
|
||||
const { cattleExitApi } = require('../../../services/api')
|
||||
const { formatDate, formatTime } = require('../../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
records: [],
|
||||
loading: false,
|
||||
searchEarNumber: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
pages: [],
|
||||
lastPage: 1
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 规范化时间戳,兼容秒/毫秒
|
||||
normalizeTs(ts) {
|
||||
if (!ts) return ''
|
||||
const n = Number(ts)
|
||||
if (!Number.isFinite(n)) return ''
|
||||
return n < 10_000_000_000 ? n * 1000 : n
|
||||
},
|
||||
|
||||
// 兼容数值时间戳与 ISO 字符串
|
||||
formatAnyTime(v) {
|
||||
if (!v) return ''
|
||||
if (typeof v === 'number') {
|
||||
const n = this.normalizeTs(v)
|
||||
return n ? `${formatDate(n)} ${formatTime(n)}` : ''
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
const p = Date.parse(v)
|
||||
if (!Number.isNaN(p)) {
|
||||
return `${formatDate(p)} ${formatTime(p)}`
|
||||
}
|
||||
return v
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
async loadRecords() {
|
||||
this.setData({ loading: true })
|
||||
const { page, pageSize, searchEarNumber } = this.data
|
||||
try {
|
||||
// 支持分页与耳号精确查询(search)
|
||||
const resp = await cattleExitApi.getExitRecords({ page, pageSize, search: searchEarNumber })
|
||||
|
||||
let list = []
|
||||
let total = 0
|
||||
let totalPagesOverride = 0
|
||||
let isUnknownTotal = false
|
||||
|
||||
if (Array.isArray(resp)) {
|
||||
list = resp
|
||||
isUnknownTotal = true
|
||||
} else if (resp && typeof resp === 'object') {
|
||||
const data = resp.data || resp
|
||||
list = data.list || data.records || data.items || []
|
||||
total = Number(data.total || data.count || data.totalCount || 0)
|
||||
if (total > 0) {
|
||||
totalPagesOverride = Math.ceil(total / pageSize)
|
||||
} else {
|
||||
isUnknownTotal = true
|
||||
}
|
||||
}
|
||||
|
||||
let mapped = (list || []).map(this.mapRecord.bind(this))
|
||||
|
||||
// 前端再次做耳号精确过滤,确保“精确查询”要求
|
||||
if (searchEarNumber && typeof searchEarNumber === 'string') {
|
||||
const kw = searchEarNumber.trim()
|
||||
if (kw) {
|
||||
mapped = mapped.filter(r => String(r.earNumber) === kw)
|
||||
// 在精确查询场景下通常结果有限,分页根据当前页构造
|
||||
isUnknownTotal = true
|
||||
total = mapped.length
|
||||
totalPagesOverride = 1
|
||||
}
|
||||
}
|
||||
|
||||
const hasMore = isUnknownTotal ? (mapped.length >= pageSize) : (page < Math.max(1, totalPagesOverride))
|
||||
|
||||
this.setData({
|
||||
records: mapped,
|
||||
total: total || 0,
|
||||
hasMore
|
||||
})
|
||||
this.buildPagination(total || 0, totalPagesOverride, isUnknownTotal)
|
||||
} catch (error) {
|
||||
console.error('获取离栏记录失败:', error)
|
||||
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 字段映射(显示全部返回字段),并尽量做中文映射与嵌套对象处理
|
||||
mapRecord(item = {}) {
|
||||
const normalize = (v) => (v === null || v === undefined || v === '') ? '-' : v
|
||||
|
||||
// 主展示字段(可能包含嵌套对象)
|
||||
const earNumber = item.earNumber || item.earNo || item.earTag || '-'
|
||||
const exitPen = (item.originalPen && (item.originalPen.name || item.originalPen.code))
|
||||
|| (item.pen && (item.pen.name || item.pen.code))
|
||||
|| item.originalPenName || item.penName || item.barnName || item.pen || item.barn || '-'
|
||||
const exitTime = item.exitDate || item.exitTime || item.exitAt || item.createdAt || item.createTime || item.time || item.created_at || ''
|
||||
|
||||
const labelMap = {
|
||||
id: '记录ID',
|
||||
recordId: '记录编号',
|
||||
animalId: '动物ID', cattleId: '牛只ID',
|
||||
earNumber: '耳号', earNo: '耳号', earTag: '耳号',
|
||||
penName: '栏舍', barnName: '栏舍', pen: '栏舍', barn: '栏舍', penId: '栏舍ID',
|
||||
originalPen: '原栏舍', originalPenId: '原栏舍ID',
|
||||
exitDate: '离栏时间', exitTime: '离栏时间', exitAt: '离栏时间', time: '离栏时间',
|
||||
createdAt: '创建时间', createTime: '创建时间', updatedAt: '更新时间', updateTime: '更新时间',
|
||||
created_at: '创建时间', updated_at: '更新时间',
|
||||
operator: '操作人', operatorId: '操作人ID', handler: '经办人',
|
||||
remark: '备注', note: '备注', reason: '原因', exitReason: '离栏原因', status: '状态',
|
||||
disposalMethod: '处置方式', destination: '去向',
|
||||
batchId: '批次ID', batchName: '批次',
|
||||
farmId: '农场ID', farmName: '农场',
|
||||
deviceNumber: '设备编号', deviceSn: '设备编号', deviceId: '设备ID',
|
||||
weight: '体重(kg)'
|
||||
}
|
||||
|
||||
const displayFields = []
|
||||
Object.keys(item).forEach((key) => {
|
||||
let value = item[key]
|
||||
// 嵌套对象处理
|
||||
if (key === 'farm' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'farm.name', label: '农场', value: normalize(value.name) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'farm.id', label: '农场ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
if (key === 'pen' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'pen.name', label: '栏舍', value: normalize(value.name) })
|
||||
if (value.code) displayFields.push({ key: 'pen.code', label: '栏舍编码', value: normalize(value.code) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'pen.id', label: '栏舍ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
if (key === 'originalPen' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'originalPen.name', label: '原栏舍', value: normalize(value.name) })
|
||||
if (value.code) displayFields.push({ key: 'originalPen.code', label: '原栏舍编码', value: normalize(value.code) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'originalPen.id', label: '原栏舍ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
|
||||
// 时间字段统一格式化
|
||||
if (/time|At|Date$|_at$/i.test(key)) {
|
||||
value = this.formatAnyTime(value) || '-'
|
||||
}
|
||||
// 将 ID 字段展示为对应的名称
|
||||
if (key === 'farmId') {
|
||||
displayFields.push({ key: 'farmId', label: '农场', value: normalize((item.farm && item.farm.name) || value) })
|
||||
return
|
||||
}
|
||||
if (key === 'penId') {
|
||||
displayFields.push({ key: 'penId', label: '栏舍', value: normalize((item.pen && item.pen.name) || value) })
|
||||
return
|
||||
}
|
||||
|
||||
displayFields.push({
|
||||
key,
|
||||
label: labelMap[key] || key,
|
||||
value: normalize(value)
|
||||
})
|
||||
})
|
||||
|
||||
const headerKeys = [
|
||||
'earNumber', 'earNo', 'earTag',
|
||||
'penName', 'barnName', 'pen', 'barn', 'pen.name', 'originalPen.name',
|
||||
'exitDate', 'exitTime', 'exitAt', 'time', 'createdAt', 'created_at', 'farm.name',
|
||||
'recordId', 'status'
|
||||
]
|
||||
const details = displayFields.filter(f => !headerKeys.includes(f.key))
|
||||
|
||||
return {
|
||||
id: item.id || item._id || '-',
|
||||
recordId: item.recordId || '-',
|
||||
status: item.status || '-',
|
||||
earNumber,
|
||||
pen: exitPen,
|
||||
exitTimeStr: this.formatAnyTime(exitTime) || '-',
|
||||
operator: item.operator || item.handler || '-',
|
||||
remark: item.remark || item.note || '-',
|
||||
batchName: item.batchName || '-',
|
||||
farmName: (item.farm && item.farm.name) || item.farmName || '-',
|
||||
deviceNumber: item.deviceNumber || item.deviceSn || item.deviceId || '-',
|
||||
details,
|
||||
raw: item
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入(耳号精确查询)
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchEarNumber: e.detail.value || '' })
|
||||
},
|
||||
|
||||
onSearch() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onClearSearch() {
|
||||
this.setData({ searchEarNumber: '', page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 分页构造与交互
|
||||
buildPagination(total, totalPagesOverride = 0, isUnknownTotal = false) {
|
||||
const pageSize = this.data.pageSize
|
||||
const current = this.data.page
|
||||
if (isUnknownTotal) {
|
||||
const pages = Array.from({ length: Math.max(1, current) }, (_, i) => i + 1)
|
||||
this.setData({ pages, lastPage: current })
|
||||
return
|
||||
}
|
||||
const totalPages = Math.max(1, Number(totalPagesOverride) || Math.ceil(total / pageSize))
|
||||
let pages = []
|
||||
const maxVisible = 9
|
||||
if (totalPages <= maxVisible) {
|
||||
pages = Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
} else {
|
||||
let start = Math.max(1, current - 4)
|
||||
let end = Math.min(totalPages, start + maxVisible - 1)
|
||||
start = Math.max(1, end - maxVisible + 1)
|
||||
pages = Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
}
|
||||
this.setData({ pages, lastPage: totalPages })
|
||||
},
|
||||
|
||||
onPrevPage() {
|
||||
if (this.data.page <= 1) return
|
||||
this.setData({ page: this.data.page - 1, records: [] })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onNextPage() {
|
||||
if (!this.data.hasMore) return
|
||||
this.setData({ page: this.data.page + 1, records: [] })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onPageTap(e) {
|
||||
const p = Number(e.currentTarget.dataset.page)
|
||||
if (!Number.isFinite(p)) return
|
||||
if (p === this.data.page) return
|
||||
this.setData({ page: p, records: [] })
|
||||
this.loadRecords()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<view class="title">牛只管理 - 离栏记录</view>
|
||||
<view class="search-box">
|
||||
<input class="search-input" placeholder="输入耳号精确查询" value="{{searchEarNumber}}" bindinput="onSearchInput" />
|
||||
<button class="search-btn" bindtap="onSearch">查询</button>
|
||||
<button class="clear-btn" bindtap="onClearSearch">清空</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list" wx:if="{{records && records.length > 0}}">
|
||||
<block wx:for="{{records}}" wx:key="id">
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<view class="left">
|
||||
<view class="main">耳号:{{item.earNumber}}</view>
|
||||
<view class="sub">离栏栏舍:{{item.pen}}</view>
|
||||
</view>
|
||||
<view class="right">
|
||||
<view class="meta">状态:{{item.status ? item.status : '-'}} | 记录编号:{{item.recordId ? item.recordId : '-'}} | 农场:{{item.farmName ? item.farmName : '-'}} </view>
|
||||
<view class="time">离栏时间:{{item.exitTimeStr}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="details">
|
||||
<block wx:for="{{item.details}}" wx:key="key">
|
||||
<view class="row">
|
||||
<text class="label">{{item.label}}</text>
|
||||
<text class="value">{{item.value}}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<view class="empty" wx:else>暂无数据</view>
|
||||
|
||||
<view class="pagination">
|
||||
<button class="prev" bindtap="onPrevPage" disabled="{{page<=1}}">上一页</button>
|
||||
<block wx:for="{{pages}}" wx:key="*this">
|
||||
<view class="page-item {{page==item ? 'active' : ''}}" data-page="{{item}}" bindtap="onPageTap">{{item}}</view>
|
||||
</block>
|
||||
<button class="next" bindtap="onNextPage" disabled="{{!hasMore}}">下一页</button>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view class="tips">总数:{{total}};当前页:{{page}} / {{lastPage}}</view>
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,32 @@
|
||||
page {
|
||||
background: #f7f8fa;
|
||||
}
|
||||
.header { padding: 12px; background: #fff; border-bottom: 1px solid #eee; }
|
||||
.title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
||||
.search-box { display: flex; gap: 8px; }
|
||||
.search-input { flex: 1; border: 1px solid #ddd; padding: 6px 8px; border-radius: 4px; }
|
||||
.search-btn, .clear-btn { padding: 6px 12px; border-radius: 4px; background: #1677ff; color: #fff; }
|
||||
.clear-btn { background: #999; }
|
||||
|
||||
.list { padding: 12px; }
|
||||
.card { background: #fff; border-radius: 8px; padding: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); margin-bottom: 12px; }
|
||||
.card-header { display: flex; justify-content: space-between; border-bottom: 1px dashed #eee; padding-bottom: 8px; margin-bottom: 8px; }
|
||||
.left .main { font-size: 16px; font-weight: bold; }
|
||||
.left .sub { font-size: 14px; color: #666; margin-top: 4px; }
|
||||
.right { text-align: right; }
|
||||
.right .meta { font-size: 12px; color: #333; }
|
||||
.right .time { font-size: 12px; color: #666; margin-top: 4px; }
|
||||
|
||||
.details .row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px dashed #f0f0f0; }
|
||||
.details .row:last-child { border-bottom: none; }
|
||||
.label { color: #666; }
|
||||
.value { color: #111; font-weight: 500; }
|
||||
|
||||
.pagination { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px; }
|
||||
.page-item { padding: 4px 10px; border-radius: 4px; background: #fff; border: 1px solid #ddd; }
|
||||
.page-item.active { background: #1677ff; color: #fff; border-color: #1677ff; }
|
||||
.prev, .next { background: #fff; border: 1px solid #ddd; padding: 6px 12px; border-radius: 4px; }
|
||||
.prev[disabled], .next[disabled] { opacity: 0.5; }
|
||||
|
||||
.footer { padding: 12px; text-align: center; color: #666; }
|
||||
.loading { margin-top: 8px; }
|
||||
285
mini_program/farm-monitor-dashboard/pages/cattle/pens/pens.js
Normal file
285
mini_program/farm-monitor-dashboard/pages/cattle/pens/pens.js
Normal file
@@ -0,0 +1,285 @@
|
||||
// pages/cattle/pens/pens.js - 栏舍设置页面
|
||||
const { cattlePenApi } = require('../../../services/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
records: [],
|
||||
displayRecords: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
pages: [],
|
||||
currentPage: 1,
|
||||
searchName: '',
|
||||
hasMore: false,
|
||||
headerKeys: ['name', 'code', 'farmName', 'status', 'capacity', 'createdAtStr'],
|
||||
paginationWindow: 7
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadRecords().finally(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
const { page, hasMore } = this.data
|
||||
if (hasMore) {
|
||||
this.setData({ page: page + 1 })
|
||||
this.loadRecords()
|
||||
}
|
||||
},
|
||||
|
||||
// 统一时间格式化
|
||||
formatAnyTime(val) {
|
||||
if (!val) return ''
|
||||
try {
|
||||
const d = typeof val === 'string' ? new Date(val) : new Date(val)
|
||||
if (isNaN(d.getTime())) return String(val)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mi = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${y}-${m}-${dd} ${hh}:${mi}`
|
||||
} catch (e) {
|
||||
return String(val)
|
||||
}
|
||||
},
|
||||
|
||||
// 字段中文映射
|
||||
getLabelMap() {
|
||||
return {
|
||||
id: '栏舍ID',
|
||||
name: '栏舍名称',
|
||||
penName: '栏舍名称',
|
||||
code: '栏舍编码',
|
||||
penId: '栏舍编号',
|
||||
barnId: '所在牛舍ID',
|
||||
barn: '所在牛舍',
|
||||
farmId: '养殖场ID',
|
||||
farm_id: '养殖场ID',
|
||||
farm: '养殖场',
|
||||
capacity: '容量',
|
||||
status: '状态',
|
||||
type: '类型',
|
||||
location: '位置',
|
||||
area: '面积',
|
||||
currentCount: '当前数量',
|
||||
manager: '负责人',
|
||||
contact: '联系电话',
|
||||
phone: '联系电话',
|
||||
remark: '备注',
|
||||
note: '备注',
|
||||
createdAt: '创建时间',
|
||||
created_at: '创建时间',
|
||||
updatedAt: '更新时间',
|
||||
updated_at: '更新时间',
|
||||
createdTime: '创建时间',
|
||||
updatedTime: '更新时间',
|
||||
isActive: '是否启用',
|
||||
enabled: '是否启用',
|
||||
disabled: '是否禁用'
|
||||
}
|
||||
},
|
||||
|
||||
// 映射单条记录为展示结构
|
||||
mapRecord(raw) {
|
||||
const labelMap = this.getLabelMap()
|
||||
const rec = { ...raw }
|
||||
// 头部主信息
|
||||
const name = rec.name || rec.penName || rec.pen || rec.title || ''
|
||||
const code = rec.code || rec.penCode || rec.number || rec.no || ''
|
||||
const capacity = rec.capacity || rec.size || rec.maxCapacity || ''
|
||||
const status = rec.status || rec.state || rec.enable || rec.enabled
|
||||
let farmName = ''
|
||||
if (rec.farm && typeof rec.farm === 'object') {
|
||||
farmName = rec.farm.name || rec.farm.title || rec.farm.code || ''
|
||||
} else if (rec.farmName) {
|
||||
farmName = rec.farmName
|
||||
}
|
||||
const createdAtStr = this.formatAnyTime(
|
||||
rec.createdAt || rec.createdTime || rec.createTime || rec.created_at
|
||||
)
|
||||
|
||||
// 详情区:将所有可用字段转为 label + value
|
||||
const displayFields = []
|
||||
|
||||
const pushField = (key, value) => {
|
||||
if (value === undefined || value === null || value === '') return
|
||||
const label = labelMap[key] || key
|
||||
// 时间字段统一格式(兼容驼峰/下划线)
|
||||
const isTimeKey = /(time|Time|createdAt|updatedAt|created_at|updated_at|createTime|updateTime|create_time|update_time)/i.test(key)
|
||||
const v = isTimeKey ? this.formatAnyTime(value) : value
|
||||
displayFields.push({ key, label, value: v })
|
||||
}
|
||||
|
||||
// 扁平字段
|
||||
Object.keys(rec).forEach(k => {
|
||||
const val = rec[k]
|
||||
if (typeof val !== 'object' || val === null) {
|
||||
// 避免重复添加已在头部展示的字段
|
||||
if (['name', 'penName', 'code', 'penCode'].includes(k)) return
|
||||
pushField(k, val)
|
||||
}
|
||||
})
|
||||
|
||||
// 嵌套对象:farm、barn 等
|
||||
const pushNested = (obj, prefixLabel) => {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
const nameV = obj.name || obj.title || obj.code
|
||||
const idV = obj.id
|
||||
if (nameV) displayFields.push({ key: `${prefixLabel}Name`, label: `${prefixLabel}名称`, value: nameV })
|
||||
if (obj.code) displayFields.push({ key: `${prefixLabel}Code`, label: `${prefixLabel}编码`, value: obj.code })
|
||||
if (idV) displayFields.push({ key: `${prefixLabel}Id`, label: `${prefixLabel}ID`, value: idV })
|
||||
}
|
||||
|
||||
pushNested(rec.farm, '养殖场')
|
||||
pushNested(rec.barn, '所在牛舍')
|
||||
|
||||
return {
|
||||
name,
|
||||
code,
|
||||
capacity,
|
||||
status,
|
||||
farmName,
|
||||
createdAtStr,
|
||||
displayFields
|
||||
}
|
||||
},
|
||||
|
||||
// 构建分页页码
|
||||
buildPagination(total, pageSize, currentPage) {
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
||||
const windowSize = this.data.paginationWindow || 7
|
||||
let start = Math.max(1, currentPage - Math.floor(windowSize / 2))
|
||||
let end = Math.min(totalPages, start + windowSize - 1)
|
||||
// 如果窗口不足,向前回补
|
||||
start = Math.max(1, end - windowSize + 1)
|
||||
const pages = []
|
||||
for (let i = start; i <= end; i++) pages.push(i)
|
||||
return { pages, totalPages }
|
||||
},
|
||||
|
||||
// 加载数据
|
||||
async loadRecords() {
|
||||
const { page, pageSize, searchName } = this.data
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const params = { page, pageSize }
|
||||
// 后端使用 search 参数进行关键词搜索,这里与后端保持一致
|
||||
if (searchName && searchName.trim()) params.search = searchName.trim()
|
||||
|
||||
const res = await cattlePenApi.getPens(params)
|
||||
|
||||
// 统一规范化响应结构,避免 list.map 报错
|
||||
const normalize = (response) => {
|
||||
// 纯数组直接返回
|
||||
if (Array.isArray(response)) {
|
||||
return { list: response, total: response.length, pagination: { total: response.length } }
|
||||
}
|
||||
// 优先从 data 节点取值
|
||||
const dataNode = response?.data !== undefined ? response.data : response
|
||||
|
||||
// 提取列表
|
||||
let list = []
|
||||
if (Array.isArray(dataNode)) {
|
||||
list = dataNode
|
||||
} else if (Array.isArray(dataNode?.list)) {
|
||||
list = dataNode.list
|
||||
} else if (Array.isArray(dataNode?.items)) {
|
||||
list = dataNode.items
|
||||
} else if (Array.isArray(response?.list)) {
|
||||
list = response.list
|
||||
} else if (Array.isArray(response?.items)) {
|
||||
list = response.items
|
||||
} else {
|
||||
list = []
|
||||
}
|
||||
|
||||
// 提取分页与总数
|
||||
const pagination = dataNode?.pagination || response?.pagination || {}
|
||||
const total = (
|
||||
pagination?.total ??
|
||||
dataNode?.total ??
|
||||
response?.total ??
|
||||
(typeof dataNode?.count === 'number' ? dataNode.count : undefined) ??
|
||||
(typeof response?.count === 'number' ? response.count : undefined) ??
|
||||
list.length
|
||||
)
|
||||
|
||||
return { list, total, pagination }
|
||||
}
|
||||
|
||||
const { list, total, pagination } = normalize(res)
|
||||
|
||||
// 映射展示结构
|
||||
const safeList = Array.isArray(list) ? list : []
|
||||
let mapped = safeList.map(item => this.mapRecord(item))
|
||||
|
||||
// 前端严格精确查询(避免远程不支持或模糊匹配)
|
||||
if (searchName && searchName.trim()) {
|
||||
const kw = searchName.trim()
|
||||
mapped = mapped.filter(r => String(r.name) === kw)
|
||||
}
|
||||
|
||||
const { pages, totalPages } = this.buildPagination(total, pageSize, page)
|
||||
const hasMore = page < totalPages
|
||||
|
||||
this.setData({
|
||||
records: safeList,
|
||||
displayRecords: mapped,
|
||||
total,
|
||||
pages,
|
||||
currentPage: page,
|
||||
hasMore,
|
||||
loading: false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取栏舍数据失败:', error)
|
||||
wx.showToast({ title: error.message || '获取失败', icon: 'none' })
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入变更
|
||||
onSearchInput(e) {
|
||||
const v = e.detail?.value ?? ''
|
||||
this.setData({ searchName: v })
|
||||
},
|
||||
|
||||
// 执行搜索(精确)
|
||||
onSearchConfirm() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 切换页码
|
||||
onPageTap(e) {
|
||||
const p = Number(e.currentTarget.dataset.page)
|
||||
if (!p || p === this.data.currentPage) return
|
||||
this.setData({ page: p })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onPrevPage() {
|
||||
const { currentPage } = this.data
|
||||
if (currentPage > 1) {
|
||||
this.setData({ page: currentPage - 1 })
|
||||
this.loadRecords()
|
||||
}
|
||||
},
|
||||
|
||||
onNextPage() {
|
||||
const { currentPage, pages } = this.data
|
||||
const max = pages.length ? Math.max(...pages) : currentPage
|
||||
if (currentPage < max) {
|
||||
this.setData({ page: currentPage + 1 })
|
||||
this.loadRecords()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "栏舍设置",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<view class="page">
|
||||
<!-- 头部与搜索栏 -->
|
||||
<view class="header">
|
||||
<text class="title">牛只管理 · 栏舍设置</text>
|
||||
<view class="search-bar">
|
||||
<input class="search-input" placeholder="输入栏舍名称(精确)" bindinput="onSearchInput" confirm-type="search" bindconfirm="onSearchConfirm" value="{{searchName}}" />
|
||||
<button class="search-btn" bindtap="onSearchConfirm">查询</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表 -->
|
||||
<view class="list">
|
||||
<block wx:for="{{displayRecords}}" wx:key="index">
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<view class="main">
|
||||
<text class="name">栏舍:{{item.name || '-'}}</text>
|
||||
<text class="code">编码:{{item.code || '-'}}</text>
|
||||
</view>
|
||||
<view class="meta">
|
||||
<text>养殖场:{{item.farmName || '-'}} </text>
|
||||
<text>状态:{{item.status || '-'}} </text>
|
||||
<text>容量:{{item.capacity || '-'}} </text>
|
||||
</view>
|
||||
<view class="time">
|
||||
<text>创建时间:{{item.createdAtStr || '-'}} </text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<block wx:for="{{item.displayFields}}" wx:key="key">
|
||||
<view class="row">
|
||||
<text class="label">{{item.label}}:</text>
|
||||
<text class="value">{{item.value}}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view wx:if="{{!displayRecords || displayRecords.length === 0}}" class="empty">暂无数据</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页 -->
|
||||
<view class="pagination" wx:if="{{pages && pages.length}}">
|
||||
<button class="pager-btn" bindtap="onPrevPage" disabled="{{currentPage<=1}}">上一页</button>
|
||||
<block wx:for="{{pages}}" wx:key="page">
|
||||
<button class="page-item {{currentPage === item ? 'active' : ''}}" data-page="{{item}}" bindtap="onPageTap">{{item}}</button>
|
||||
</block>
|
||||
<button class="pager-btn" bindtap="onNextPage" disabled="{{!hasMore}}">下一页</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,27 @@
|
||||
/* 栏舍设置页面样式 */
|
||||
.page { padding: 12px; background: #f7f8fa; min-height: 100vh; }
|
||||
.header { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
|
||||
.title { font-size: 16px; font-weight: 600; color: #333; }
|
||||
.search-bar { display: flex; gap: 8px; }
|
||||
.search-input { flex: 1; border: 1px solid #ddd; border-radius: 6px; padding: 8px 10px; background: #fff; }
|
||||
.search-btn { padding: 8px 12px; background: #3cc51f; color: #fff; border-radius: 6px; }
|
||||
|
||||
.list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.card { background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); padding: 12px; }
|
||||
.card-header { display: flex; flex-direction: column; gap: 6px; border-bottom: 1px dashed #eee; padding-bottom: 8px; }
|
||||
.main { display: flex; gap: 10px; align-items: baseline; }
|
||||
.name { font-size: 16px; font-weight: 600; color: #222; }
|
||||
.code { font-size: 14px; color: #666; }
|
||||
.meta { display: flex; flex-wrap: wrap; gap: 10px; color: #666; font-size: 13px; }
|
||||
.time { color: #999; font-size: 12px; }
|
||||
|
||||
.card-body { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
|
||||
.row { display: flex; gap: 8px; }
|
||||
.label { color: #666; min-width: 84px; text-align: right; }
|
||||
.value { color: #333; flex: 1; }
|
||||
|
||||
.pagination { display: flex; gap: 6px; justify-content: center; align-items: center; margin-top: 14px; }
|
||||
.pager-btn { background: #f0f0f0; color: #333; border-radius: 6px; padding: 6px 10px; }
|
||||
.page-item { background: #fff; color: #333; border: 1px solid #ddd; border-radius: 6px; padding: 6px 10px; }
|
||||
.page-item.active { background: #3cc51f; border-color: #3cc51f; color: #fff; }
|
||||
.empty { text-align: center; color: #999; padding: 20px 0; }
|
||||
@@ -0,0 +1,257 @@
|
||||
// pages/cattle/transfer/transfer.js
|
||||
const { cattleTransferApi } = require('../../../services/api')
|
||||
const { formatDate, formatTime } = require('../../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
records: [],
|
||||
loading: false,
|
||||
searchEarNumber: '',
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
pages: [],
|
||||
lastPage: 1
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 规范化时间戳,兼容秒/毫秒
|
||||
normalizeTs(ts) {
|
||||
if (!ts) return ''
|
||||
const n = Number(ts)
|
||||
if (!Number.isFinite(n)) return ''
|
||||
return n < 10_000_000_000 ? n * 1000 : n
|
||||
},
|
||||
|
||||
// 兼容数值时间戳与 ISO 字符串
|
||||
formatAnyTime(v) {
|
||||
if (!v) return ''
|
||||
if (typeof v === 'number') {
|
||||
const n = this.normalizeTs(v)
|
||||
return n ? `${formatDate(n)} ${formatTime(n)}` : ''
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
const p = Date.parse(v)
|
||||
if (!Number.isNaN(p)) {
|
||||
return `${formatDate(p)} ${formatTime(p)}`
|
||||
}
|
||||
// 非法字符串,直接返回原值
|
||||
return v
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
async loadRecords() {
|
||||
this.setData({ loading: true })
|
||||
const { page, pageSize, searchEarNumber } = this.data
|
||||
try {
|
||||
// 使用统一的列表接口,并传递 search 参数以满足“耳号精确查询”需求
|
||||
const resp = await cattleTransferApi.getTransferRecords({ page, pageSize, search: searchEarNumber })
|
||||
|
||||
let list = []
|
||||
let total = 0
|
||||
let totalPagesOverride = 0
|
||||
let isUnknownTotal = false
|
||||
|
||||
if (Array.isArray(resp)) {
|
||||
list = resp
|
||||
// 未返回总数时,认为总数未知;是否有更多由本页数量是否满页来推断
|
||||
isUnknownTotal = true
|
||||
} else if (resp && typeof resp === 'object') {
|
||||
// 常见结构兼容:{ list, total } 或 { data: { list, total } } 或 { records, total }
|
||||
const data = resp.data || resp
|
||||
list = data.list || data.records || data.items || []
|
||||
total = Number(data.total || data.count || data.totalCount || 0)
|
||||
if (total > 0) {
|
||||
totalPagesOverride = Math.ceil(total / pageSize)
|
||||
} else {
|
||||
isUnknownTotal = true
|
||||
}
|
||||
}
|
||||
|
||||
const mapped = (list || []).map(this.mapRecord.bind(this))
|
||||
const hasMore = isUnknownTotal ? (mapped.length >= pageSize) : (page < Math.max(1, totalPagesOverride))
|
||||
|
||||
this.setData({
|
||||
records: mapped,
|
||||
total: total || 0,
|
||||
hasMore
|
||||
})
|
||||
this.buildPagination(total || 0, totalPagesOverride, isUnknownTotal)
|
||||
} catch (error) {
|
||||
console.error('获取转栏记录失败:', error)
|
||||
wx.showToast({ title: error.message || '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 字段映射(尽量覆盖常见后端字段命名),并保留原始字段用于“全部显示”
|
||||
mapRecord(item = {}) {
|
||||
const normalize = (v) => (v === null || v === undefined || v === '') ? '-' : v
|
||||
|
||||
// 头部主展示字段(兼容嵌套对象与旧字段)
|
||||
const earNumber = item.earNumber || item.earNo || item.earTag || '-'
|
||||
const fromPen = (item.fromPen && (item.fromPen.name || item.fromPen.code))
|
||||
|| item.fromPenName || item.fromBarnName || item.fromPen || item.fromBarn || '-'
|
||||
const toPen = (item.toPen && (item.toPen.name || item.toPen.code))
|
||||
|| item.toPenName || item.toBarnName || item.toPen || item.toBarn || '-'
|
||||
const transferTime = item.transferDate || item.transferTime || item.transferAt || item.createdAt || item.createTime || item.time || item.created_at || ''
|
||||
|
||||
// 友好中文标签映射(补充新结构字段)
|
||||
const labelMap = {
|
||||
id: '记录ID',
|
||||
recordId: '记录编号',
|
||||
animalId: '动物ID',
|
||||
cattleId: '牛只ID',
|
||||
earNumber: '耳号', earNo: '耳号', earTag: '耳号',
|
||||
fromPenName: '原栏舍', fromBarnName: '原栏舍', fromPen: '原栏舍', fromBarn: '原栏舍', fromPenId: '原栏舍ID',
|
||||
toPenName: '目标栏舍', toBarnName: '目标栏舍', toPen: '目标栏舍', toBarn: '目标栏舍', toPenId: '目标栏舍ID',
|
||||
transferDate: '转栏时间', transferTime: '转栏时间', transferAt: '转栏时间', time: '转栏时间',
|
||||
createdAt: '创建时间', createTime: '创建时间', updatedAt: '更新时间', updateTime: '更新时间',
|
||||
created_at: '创建时间', updated_at: '更新时间',
|
||||
operator: '操作人', operatorId: '操作人ID', handler: '经办人',
|
||||
remark: '备注', note: '备注', reason: '原因', status: '状态',
|
||||
batchId: '批次ID', batchName: '批次',
|
||||
farmId: '农场ID', farmName: '农场',
|
||||
deviceNumber: '设备编号', deviceSn: '设备编号', deviceId: '设备ID',
|
||||
weightBefore: '转前体重(kg)', weightAfter: '转后体重(kg)', weight: '体重(kg)'
|
||||
}
|
||||
|
||||
// 将所有字段整理为可展示的键值对,时间戳/ISO 自动格式化
|
||||
const displayFields = []
|
||||
Object.keys(item).forEach((key) => {
|
||||
let value = item[key]
|
||||
// 嵌套对象特殊处理
|
||||
if (key === 'farm' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'farm.name', label: '农场', value: normalize(value.name) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'farm.id', label: '农场ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
if (key === 'fromPen' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'fromPen.name', label: '原栏舍', value: normalize(value.name) })
|
||||
if (value.code) displayFields.push({ key: 'fromPen.code', label: '原栏舍编码', value: normalize(value.code) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'fromPen.id', label: '原栏舍ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
if (key === 'toPen' && value && typeof value === 'object') {
|
||||
displayFields.push({ key: 'toPen.name', label: '目标栏舍', value: normalize(value.name) })
|
||||
if (value.code) displayFields.push({ key: 'toPen.code', label: '目标栏舍编码', value: normalize(value.code) })
|
||||
if (value.id !== undefined) displayFields.push({ key: 'toPen.id', label: '目标栏舍ID', value: normalize(value.id) })
|
||||
return
|
||||
}
|
||||
|
||||
// 时间字段格式化(兼容 ISO 字符串)
|
||||
if (/time|At|Date$|_at$/i.test(key)) {
|
||||
value = this.formatAnyTime(value) || '-'
|
||||
}
|
||||
// 将 ID 字段展示为对应的名称
|
||||
if (key === 'farmId') {
|
||||
displayFields.push({ key: 'farmId', label: '农场', value: normalize((item.farm && item.farm.name) || value) })
|
||||
return
|
||||
}
|
||||
if (key === 'fromPenId') {
|
||||
displayFields.push({ key: 'fromPenId', label: '原栏舍', value: normalize((item.fromPen && item.fromPen.name) || value) })
|
||||
return
|
||||
}
|
||||
if (key === 'toPenId') {
|
||||
displayFields.push({ key: 'toPenId', label: '目标栏舍', value: normalize((item.toPen && item.toPen.name) || value) })
|
||||
return
|
||||
}
|
||||
displayFields.push({
|
||||
key,
|
||||
label: labelMap[key] || key,
|
||||
value: normalize(value)
|
||||
})
|
||||
})
|
||||
|
||||
// 去重:头部已展示的字段不在详情中重复
|
||||
const headerKeys = [
|
||||
'earNumber', 'earNo', 'earTag',
|
||||
'fromPenName', 'fromBarnName', 'fromPen', 'fromBarn', 'fromPen.name',
|
||||
'toPenName', 'toBarnName', 'toPen', 'toBarn', 'toPen.name',
|
||||
'transferDate', 'transferTime', 'transferAt', 'time', 'createdAt', 'created_at', 'farm.name'
|
||||
]
|
||||
const details = displayFields.filter(f => !headerKeys.includes(f.key))
|
||||
|
||||
return {
|
||||
id: item.id || item._id || '-',
|
||||
recordId: item.recordId || '-',
|
||||
status: item.status || '-',
|
||||
earNumber,
|
||||
fromPen,
|
||||
toPen,
|
||||
transferTimeStr: this.formatAnyTime(transferTime) || '-',
|
||||
operator: item.operator || item.handler || '-',
|
||||
remark: item.remark || item.note || '-',
|
||||
batchName: item.batchName || '-',
|
||||
farmName: (item.farm && item.farm.name) || item.farmName || '-',
|
||||
deviceNumber: item.deviceNumber || item.deviceSn || item.deviceId || '-',
|
||||
details,
|
||||
raw: item
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchEarNumber: e.detail.value || '' })
|
||||
},
|
||||
|
||||
// 执行搜索(耳号精确查询)
|
||||
onSearch() {
|
||||
this.setData({ page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
onClearSearch() {
|
||||
this.setData({ searchEarNumber: '', page: 1 })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
// 分页构造与交互
|
||||
buildPagination(total, totalPagesOverride = 0, isUnknownTotal = false) {
|
||||
const pageSize = this.data.pageSize
|
||||
const current = this.data.page
|
||||
if (isUnknownTotal) {
|
||||
const pages = Array.from({ length: Math.max(1, current) }, (_, i) => i + 1)
|
||||
this.setData({ pages, lastPage: current })
|
||||
return
|
||||
}
|
||||
const totalPages = Math.max(1, Number(totalPagesOverride) || Math.ceil(total / pageSize))
|
||||
let pages = []
|
||||
const maxVisible = 9
|
||||
if (totalPages <= maxVisible) {
|
||||
pages = Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
} else {
|
||||
let start = Math.max(1, current - 4)
|
||||
let end = Math.min(totalPages, start + maxVisible - 1)
|
||||
start = Math.max(1, end - maxVisible + 1)
|
||||
pages = Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
}
|
||||
this.setData({ pages, lastPage: totalPages })
|
||||
},
|
||||
|
||||
onPrevPage() {
|
||||
if (this.data.page <= 1) return
|
||||
this.setData({ page: this.data.page - 1, records: [] })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onNextPage() {
|
||||
if (!this.data.hasMore) return
|
||||
this.setData({ page: this.data.page + 1, records: [] })
|
||||
this.loadRecords()
|
||||
},
|
||||
|
||||
onPageTap(e) {
|
||||
const targetPage = Number(e.currentTarget.dataset.page)
|
||||
if (!targetPage || targetPage === this.data.page) return
|
||||
this.setData({ page: targetPage, records: [] })
|
||||
this.loadRecords()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<!-- pages/cattle/transfer/transfer.wxml -->
|
||||
<view class="transfer-container">
|
||||
<!-- 搜索栏:耳号精确查询 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input-wrapper">
|
||||
<input class="search-input" placeholder="请输入耳号,精确查询" value="{{searchEarNumber}}" bindinput="onSearchInput" confirm-type="search" bindconfirm="onSearch"/>
|
||||
<text class="search-icon" bindtap="onSearch">🔍</text>
|
||||
</view>
|
||||
<text class="clear-btn" bindtap="onClearSearch" wx:if="{{searchEarNumber}}">清空</text>
|
||||
</view>
|
||||
|
||||
<!-- 列表 -->
|
||||
<view class="record-list">
|
||||
<block wx:if="{{records.length > 0}}">
|
||||
<block wx:for="{{records}}" wx:key="id">
|
||||
<view class="record-item">
|
||||
<!-- 左侧头像/图标 -->
|
||||
<view class="record-avatar"><text class="avatar-icon">🏠</text></view>
|
||||
|
||||
<!-- 中间信息 -->
|
||||
<view class="record-info">
|
||||
<view class="record-title">
|
||||
<text class="ear-number">耳号:{{item.earNumber}}</text>
|
||||
</view>
|
||||
<!-- 扩展详情(全部字段展示,已去重头部字段) -->
|
||||
<view class="record-extra">
|
||||
<block wx:for="{{item.details}}" wx:key="{{item.key}}">
|
||||
<view class="extra-row">
|
||||
<text class="extra-label">{{item.label}}:</text>
|
||||
<text class="extra-value">{{item.value}}</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧元信息 -->
|
||||
<view class="record-meta">
|
||||
<text class="meta-item">状态:{{item.status || '-'}} </text>
|
||||
<text class="meta-item">编号:{{item.recordId || '-'}} </text>
|
||||
<text class="meta-item">农场:{{item.farmName || '-'}} </text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 加载更多/无更多 -->
|
||||
<view wx:if="{{hasMore}}" class="load-more">
|
||||
<text wx:if="{{loading}}">加载中...</text>
|
||||
<text wx:else>上拉或点击页码加载更多</text>
|
||||
</view>
|
||||
<view wx:if="{{!hasMore}}" class="no-more">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view wx:else class="empty-state">
|
||||
<text class="empty-icon">📄</text>
|
||||
<text class="empty-text">暂无转栏记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分页导航 -->
|
||||
<view wx:if="{{records.length > 0}}" class="pagination">
|
||||
<view class="page-item {{page <= 1 ? 'disabled' : ''}}" bindtap="onPrevPage">上一页</view>
|
||||
<view class="page-item" data-page="1" bindtap="onPageTap">首页</view>
|
||||
<view
|
||||
wx:for="{{pages}}"
|
||||
wx:key="*this"
|
||||
class="page-item {{item === page ? 'active' : ''}}"
|
||||
data-page="{{item}}"
|
||||
bindtap="onPageTap"
|
||||
>{{item}}</view>
|
||||
<view class="page-item" data-page="{{lastPage}}" bindtap="onPageTap">末页</view>
|
||||
<view class="page-item {{page >= lastPage ? 'disabled' : ''}}" bindtap="onNextPage">下一页</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view wx:if="{{loading && records.length === 0}}" class="loading-container">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,211 @@
|
||||
/* pages/cattle/transfer/transfer.wxss */
|
||||
.transfer-container {
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx;
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 60rpx 0 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 32rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
font-size: 28rpx;
|
||||
color: #3cc51f;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.record-item:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.record-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background-color: #f0f9ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.record-info {
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.ear-number {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.record-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: 24rpx;
|
||||
color: #606266;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.record-extra {
|
||||
margin-top: 8rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6rpx 12rpx;
|
||||
}
|
||||
|
||||
.extra-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.extra-label {
|
||||
color: #909399;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.extra-value {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.record-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 22rpx;
|
||||
color: #909399;
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 32rpx;
|
||||
font-size: 24rpx;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
/* 分页导航 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 16rpx 40rpx;
|
||||
}
|
||||
|
||||
.page-item {
|
||||
min-width: 60rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
background-color: #f5f5f5;
|
||||
color: #606266;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-item.active {
|
||||
background-color: #3cc51f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.page-item.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 120rpx 32rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid #f0f0f0;
|
||||
border-top: 4rpx solid #3cc51f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -77,7 +77,7 @@ Page({
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const { currentPage, pageSize, searchValue } = this.data
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/collars?page=${currentPage}&limit=${pageSize}&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/smart-devices/collars?page=${currentPage}&limit=${pageSize}&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
|
||||
// 检查登录状态
|
||||
const token = wx.getStorageSync('token')
|
||||
@@ -290,7 +290,7 @@ Page({
|
||||
|
||||
// 执行精确搜索
|
||||
performExactSearch(searchValue) {
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/collars?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/smart-devices/collars?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
|
||||
// 检查登录状态
|
||||
const token = wx.getStorageSync('token')
|
||||
|
||||
@@ -66,7 +66,7 @@ Page({
|
||||
// 调用私有API获取耳标列表,然后筛选出指定耳标
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
|
||||
@@ -113,7 +113,7 @@ Page({
|
||||
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: page,
|
||||
@@ -500,7 +500,7 @@ Page({
|
||||
// 调用API获取所有数据的总数(不受分页限制)
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
@@ -621,7 +621,7 @@ Page({
|
||||
// 调用私有API获取所有数据,然后进行客户端搜索
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/smart-devices/eartags',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
|
||||
@@ -100,7 +100,7 @@ Page({
|
||||
if (!this.checkLoginStatus()) return
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
const url = `https://ad.ningmuyun.com/api/electronic-fence?page=1&limit=100&_t=${Date.now()}`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/electronic-fence?page=1&limit=100&_t=${Date.now()}`
|
||||
|
||||
this.setData({ loading: true })
|
||||
wx.request({
|
||||
|
||||
@@ -61,7 +61,7 @@ Page({
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const { currentPage, pageSize } = this.data
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/hosts?page=${currentPage}&limit=${pageSize}&_t=${Date.now()}&refresh=true`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/smart-devices/hosts?page=${currentPage}&limit=${pageSize}&_t=${Date.now()}&refresh=true`
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
@@ -303,7 +303,7 @@ Page({
|
||||
|
||||
// 执行精确搜索
|
||||
performExactSearch(searchValue) {
|
||||
const url = `https://ad.ningmuyun.com/api/smart-devices/hosts?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
const url = `https://ad.ningmuyun.com/farm/api/smart-devices/hosts?page=1&limit=1&deviceId=${searchValue}&_t=${Date.now()}`
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// pages/home/home.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { alertApi } = require('../../services/api.js')
|
||||
const { formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
@@ -123,7 +124,7 @@ Page({
|
||||
},
|
||||
|
||||
// 更新预警数据
|
||||
updateAlertData(tabIndex) {
|
||||
async updateAlertData(tabIndex) {
|
||||
let alertData = []
|
||||
|
||||
switch (tabIndex) {
|
||||
@@ -139,15 +140,9 @@ Page({
|
||||
]
|
||||
break
|
||||
case 1: // 耳标预警
|
||||
alertData = [
|
||||
{ title: '今日未被采集', value: '2', isAlert: false, bgIcon: '📄', url: '/pages/alert/eartag' },
|
||||
{ title: '耳标脱落', value: '1', isAlert: true, bgIcon: '👂', url: '/pages/alert/eartag' },
|
||||
{ title: '温度异常', value: '0', isAlert: false, bgIcon: '🌡️', url: '/pages/alert/eartag' },
|
||||
{ title: '心率异常', value: '1', isAlert: true, bgIcon: '💓', url: '/pages/alert/eartag' },
|
||||
{ title: '位置异常', value: '0', isAlert: false, bgIcon: '📍', url: '/pages/alert/eartag' },
|
||||
{ title: '电量偏低', value: '3', isAlert: false, bgIcon: '🔋', url: '/pages/alert/eartag' }
|
||||
]
|
||||
break
|
||||
// 动态调用真实耳标预警接口,使用返回数据填充首页卡片
|
||||
await this.fetchEartagAlertCards()
|
||||
return
|
||||
case 2: // 脚环预警
|
||||
alertData = [
|
||||
{ title: '今日未被采集', value: '1', isAlert: false, bgIcon: '📄', url: '/pages/alert/ankle' },
|
||||
@@ -169,6 +164,47 @@ Page({
|
||||
this.setData({ currentAlertData: alertData })
|
||||
},
|
||||
|
||||
// 动态加载耳标预警数据并映射到首页卡片
|
||||
async fetchEartagAlertCards() {
|
||||
try {
|
||||
this.setData({ loading: true })
|
||||
const params = { search: '', alertType: '', page: 1, limit: 10 }
|
||||
const res = await alertApi.getEartagAlerts(params)
|
||||
// 兼容响应结构:可能是 { success, data: [...], stats, pagination } 或直接返回 { data: [...], stats }
|
||||
const list = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : [])
|
||||
const stats = res?.stats || {}
|
||||
|
||||
// 统计映射(根据公开API字段)
|
||||
const offline = Number(stats.offline || 0) // 离线数量(近似“今日未被采集”)
|
||||
const highTemperature = Number(stats.highTemperature || 0) // 温度异常数量
|
||||
const lowBattery = Number(stats.lowBattery || 0) // 电量偏低数量
|
||||
const abnormalMovement = Number(stats.abnormalMovement || 0) // 运动异常/当日运动量为0
|
||||
|
||||
// 构建首页卡片(保持既有文案,无法统计的置0)
|
||||
const alertData = [
|
||||
{ title: '今日未被采集', value: String(offline), isAlert: offline > 0, bgIcon: '📄', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '耳标脱落', value: '0', isAlert: false, bgIcon: '👂', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '温度异常', value: String(highTemperature), isAlert: highTemperature > 0, bgIcon: '🌡️', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '心率异常', value: '0', isAlert: false, bgIcon: '💓', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '位置异常', value: '0', isAlert: false, bgIcon: '📍', url: '/pages/alert/alert?type=eartag' },
|
||||
{ title: '电量偏低', value: String(lowBattery), isAlert: lowBattery > 0, bgIcon: '🔋', url: '/pages/alert/alert?type=eartag' }
|
||||
]
|
||||
|
||||
// 如果有“运动异常”,在卡片上以“今日运动量异常”补充显示(替代心率异常)
|
||||
if (abnormalMovement > 0) {
|
||||
alertData.splice(3, 1, { title: '今日运动量异常', value: String(abnormalMovement), isAlert: true, bgIcon: '📉', url: '/pages/alert/alert?type=eartag' })
|
||||
}
|
||||
|
||||
this.setData({ currentAlertData: alertData })
|
||||
} catch (error) {
|
||||
console.error('获取耳标预警失败:', error)
|
||||
wx.showToast({ title: '耳标预警加载失败', icon: 'none' })
|
||||
// 失败时保持原有数据不变
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 导航到指定页面
|
||||
navigateTo(e) {
|
||||
const url = e.currentTarget.dataset.url
|
||||
|
||||
@@ -126,7 +126,7 @@ Page({
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/api/auth/login',
|
||||
url: 'https://ad.ningmuyun.com/farm/api/auth/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
username: username,
|
||||
|
||||
@@ -4,6 +4,30 @@ Page({
|
||||
loading: false
|
||||
},
|
||||
|
||||
// 直接跳转:牛-栏舍设置
|
||||
goCattlePenSettings() {
|
||||
console.log('准备跳转到牛-栏舍设置页面')
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/pens/pens',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 直接跳转:牛-批次设置
|
||||
goCattleBatchSettings() {
|
||||
console.log('准备跳转到牛-批次设置页面')
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/batches/batches',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('生产管理页面加载')
|
||||
},
|
||||
@@ -19,6 +43,21 @@ Page({
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
// 进入牛只管理(普通页面)
|
||||
goCattleManage() {
|
||||
// 牛只管理并非 app.json 的 tabBar 页面,使用 navigateTo 进行跳转
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/cattle',
|
||||
fail: (err) => {
|
||||
console.error('跳转牛只管理失败:', err)
|
||||
wx.showToast({
|
||||
title: '跳转失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 导航到指定页面
|
||||
navigateTo(e) {
|
||||
const url = e.currentTarget.dataset.url
|
||||
@@ -75,6 +114,7 @@ Page({
|
||||
}
|
||||
|
||||
const animalNames = {
|
||||
'cattle': '牛',
|
||||
'pig': '猪',
|
||||
'sheep': '羊',
|
||||
'poultry': '家禽'
|
||||
@@ -83,6 +123,55 @@ Page({
|
||||
const functionName = functionNames[functionType] || '未知功能'
|
||||
const animalName = animalNames[animalType] || '未知动物'
|
||||
|
||||
// 对“牛-转栏记录”和“牛-离栏记录”开放跳转,其它功能保留提示
|
||||
if (animalType === 'cattle' && functionType === 'pen-transfer') {
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/transfer/transfer',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (animalType === 'cattle' && functionType === 'pen-exit') {
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/exit/exit',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 牛-栏舍设置:跳转到新创建的栏舍设置页面
|
||||
if (animalType === 'cattle' && functionType === 'pen-settings') {
|
||||
console.log('匹配到牛-栏舍设置分支,开始跳转')
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/pens/pens',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 牛-批次设置:跳转到新创建的批次设置页面
|
||||
if (animalType === 'cattle' && functionType === 'batch-settings') {
|
||||
console.log('匹配到牛-批次设置分支,开始跳转')
|
||||
wx.navigateTo({
|
||||
url: '/pages/cattle/batches/batches',
|
||||
fail: (error) => {
|
||||
console.error('页面跳转失败:', error)
|
||||
wx.showToast({ title: '页面不存在', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.showToast({
|
||||
title: `${animalName}${functionName}功能开发中`,
|
||||
icon: 'none',
|
||||
|
||||
@@ -3,6 +3,71 @@
|
||||
<!-- 页面标题 -->
|
||||
<view class="page-title">生产管理</view>
|
||||
|
||||
<!-- 牛只管理模块 -->
|
||||
<view class="management-section">
|
||||
<view class="section-header">
|
||||
<view class="section-title-bar"></view>
|
||||
<text class="section-title">牛只管理</text>
|
||||
</view>
|
||||
|
||||
<view class="function-grid">
|
||||
<view class="function-item" bindtap="goCattleManage">
|
||||
<view class="function-icon cattle-archive">🐄</view>
|
||||
<text class="function-text">牛只档案</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="estrus-record">
|
||||
<view class="function-icon estrus-record">💗</view>
|
||||
<text class="function-text">发情记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="mating-record">
|
||||
<view class="function-icon mating-record">🧪</view>
|
||||
<text class="function-text">配种记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="pregnancy-check">
|
||||
<view class="function-icon pregnancy-check">📅</view>
|
||||
<text class="function-text">妊检记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="farrowing-record">
|
||||
<view class="function-icon farrowing-record">👶</view>
|
||||
<text class="function-text">分娩记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="weaning-record">
|
||||
<view class="function-icon weaning-record">✏️</view>
|
||||
<text class="function-text">断奶记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="pen-transfer">
|
||||
<view class="function-icon pen-transfer">🏠</view>
|
||||
<text class="function-text">转栏记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="pen-exit">
|
||||
<view class="function-icon pen-exit">🏠</view>
|
||||
<text class="function-text">离栏记录</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="goCattlePenSettings" data-animal="cattle" data-type="pen-settings">
|
||||
<view class="function-icon pen-settings">🏠</view>
|
||||
<text class="function-text">栏舍设置</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="goCattleBatchSettings" data-animal="cattle" data-type="batch-settings">
|
||||
<view class="function-icon batch-settings">📄</view>
|
||||
<text class="function-text">批次设置</text>
|
||||
</view>
|
||||
|
||||
<view class="function-item" bindtap="onFunctionClick" data-animal="cattle" data-type="epidemic-warning">
|
||||
<view class="function-icon epidemic-warning">🛡️</view>
|
||||
<text class="function-text">防疫预警</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 猪档案管理模块 -->
|
||||
<view class="management-section">
|
||||
<view class="section-header">
|
||||
|
||||
@@ -137,6 +137,11 @@
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 牛只管理图标颜色 */
|
||||
.function-icon.cattle-archive {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.function-icon.scan-entry {
|
||||
background-color: #722ed1;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const app = getApp()
|
||||
// 基础配置
|
||||
const config = {
|
||||
// 使用真实的智能耳标API接口(直接连接后端)
|
||||
baseUrl: 'https://ad.ningmuyun.com/api', // 智能耳标API地址
|
||||
baseUrl: 'https://ad.ningmuyun.com/farm/api', // 智能耳标API地址(生产环境)
|
||||
timeout: 10000,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -89,49 +89,81 @@ const responseInterceptor = (response) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
// 通用请求方法(带故障转移)
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 应用请求拦截器
|
||||
const processedOptions = requestInterceptor({
|
||||
url: config.baseUrl + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
...config.header,
|
||||
...options.header
|
||||
},
|
||||
timeout: options.timeout || config.timeout
|
||||
})
|
||||
|
||||
wx.request({
|
||||
...processedOptions,
|
||||
success: (response) => {
|
||||
console.log('wx.request成功:', response)
|
||||
try {
|
||||
const result = responseInterceptor(response)
|
||||
console.log('响应拦截器处理结果:', result)
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
console.log('响应拦截器错误:', error)
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('wx.request失败:', error)
|
||||
let message = '网络连接异常'
|
||||
|
||||
if (error.errMsg) {
|
||||
if (error.errMsg.includes('timeout')) {
|
||||
message = '请求超时'
|
||||
} else if (error.errMsg.includes('fail')) {
|
||||
message = '网络连接失败'
|
||||
const failoverBases = [
|
||||
'http://localhost:5350/api',
|
||||
'http://127.0.0.1:5350/api'
|
||||
]
|
||||
|
||||
const allowFailover = options.allowFailover !== false
|
||||
|
||||
const doWxRequest = (baseUrlToUse, isFailoverAttempt = false) => {
|
||||
// 应用请求拦截器
|
||||
const processedOptions = requestInterceptor({
|
||||
url: baseUrlToUse + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: {
|
||||
...config.header,
|
||||
...options.header
|
||||
},
|
||||
timeout: options.timeout || config.timeout
|
||||
})
|
||||
|
||||
console.log('[API] 请求URL:', processedOptions.url, '方法:', processedOptions.method, '是否故障转移:', isFailoverAttempt)
|
||||
|
||||
wx.request({
|
||||
...processedOptions,
|
||||
success: (response) => {
|
||||
console.log('[API] 响应状态码:', response.statusCode, '请求URL:', processedOptions.url)
|
||||
// 当远程网关或上游故障(5xx)时,自动尝试本地备用服务(5350)
|
||||
if (
|
||||
response.statusCode >= 500 &&
|
||||
allowFailover &&
|
||||
!isFailoverAttempt &&
|
||||
baseUrlToUse === config.baseUrl &&
|
||||
failoverBases.length > 0
|
||||
) {
|
||||
console.warn('[API] 远程服务返回', response.statusCode, ',尝试切换到本地备用服务:', failoverBases[0])
|
||||
return doWxRequest(failoverBases[0], true)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = responseInterceptor(response)
|
||||
console.log('响应拦截器处理结果:', result)
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
console.log('响应拦截器错误:', error)
|
||||
reject(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('wx.request失败:', error, '请求URL:', processedOptions.url)
|
||||
let message = '网络连接异常'
|
||||
|
||||
if (error.errMsg) {
|
||||
if (error.errMsg.includes('timeout')) {
|
||||
message = '请求超时'
|
||||
} else if (error.errMsg.includes('fail')) {
|
||||
message = '网络连接失败'
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是远程网络失败(非HTTP响应),也尝试本地备用
|
||||
if (allowFailover && !isFailoverAttempt && baseUrlToUse === config.baseUrl && failoverBases.length > 0) {
|
||||
console.warn('[API] 远程网络失败,尝试切换到本地备用服务:', failoverBases[0])
|
||||
return doWxRequest(failoverBases[0], true)
|
||||
}
|
||||
|
||||
reject(new Error(message))
|
||||
}
|
||||
|
||||
reject(new Error(message))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 首次请求使用当前配置的基础地址
|
||||
doWxRequest(config.baseUrl, false)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user