继续完善保险项目和养殖端小程序
This commit is contained in:
Binary file not shown.
@@ -175,7 +175,7 @@ const routes = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHistory('/insurance/'),
|
||||
routes
|
||||
})
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ export const applicationAPI = {
|
||||
export const policyAPI = {
|
||||
getList: (params) => api.get('/policies', { params }),
|
||||
getDetail: (id) => api.get(`/policies/${id}`),
|
||||
create: (data) => api.post('/policies', data),
|
||||
update: (id, data) => api.put(`/policies/${id}`, data),
|
||||
updateStatus: (id, data) => api.put(`/policies/${id}/status`, data),
|
||||
delete: (id) => api.delete(`/policies/${id}`)
|
||||
}
|
||||
|
||||
@@ -164,6 +164,31 @@ const fetchRequest = async (url, options = {}) => {
|
||||
// 设置默认请求头
|
||||
options.headers = createHeaders(options.headers)
|
||||
|
||||
// ========== 请求前日志 ==========
|
||||
try {
|
||||
const hasAuth = !!options.headers?.Authorization
|
||||
const contentType = options.headers?.['Content-Type']
|
||||
const acceptType = options.headers?.['Accept']
|
||||
let bodyPreview = null
|
||||
if (options.body) {
|
||||
try {
|
||||
bodyPreview = JSON.stringify(JSON.parse(options.body))
|
||||
} catch (_) {
|
||||
bodyPreview = String(options.body).slice(0, 1000)
|
||||
}
|
||||
}
|
||||
console.log('🔶 [前端] 准备发起请求', {
|
||||
method: options.method || 'GET',
|
||||
url: fullUrl,
|
||||
hasAuthorization: hasAuth ? '是' : '否',
|
||||
contentType,
|
||||
acceptType,
|
||||
bodyPreview
|
||||
})
|
||||
} catch (logError) {
|
||||
console.warn('记录请求前日志失败:', logError)
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.timeout)
|
||||
@@ -172,10 +197,66 @@ const fetchRequest = async (url, options = {}) => {
|
||||
try {
|
||||
const response = await fetch(fullUrl, options)
|
||||
clearTimeout(timeoutId)
|
||||
return await handleResponse(response)
|
||||
|
||||
// ========== 原始响应日志 ==========
|
||||
try {
|
||||
console.log('🟩 [前端] 收到原始响应', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: fullUrl,
|
||||
ok: response.ok,
|
||||
contentType: response.headers.get('content-type')
|
||||
})
|
||||
} catch (respLogErr) {
|
||||
console.warn('记录原始响应日志失败:', respLogErr)
|
||||
}
|
||||
|
||||
const result = await handleResponse(response)
|
||||
|
||||
// ========== 处理后响应日志 ==========
|
||||
try {
|
||||
const safeData = (() => {
|
||||
try {
|
||||
return typeof result.data === 'string' ? result.data.slice(0, 1000) : JSON.stringify(result.data).slice(0, 1000)
|
||||
} catch (_) {
|
||||
return '[不可序列化数据]'
|
||||
}
|
||||
})()
|
||||
console.log('✅ [前端] 请求成功', {
|
||||
url: fullUrl,
|
||||
method: options.method || 'GET',
|
||||
status: result.status,
|
||||
statusText: result.statusText,
|
||||
dataPreview: safeData
|
||||
})
|
||||
} catch (resLogErr) {
|
||||
console.warn('记录处理后响应日志失败:', resLogErr)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
// ========== 错误日志 ==========
|
||||
try {
|
||||
console.error('❌ [前端] 请求失败', {
|
||||
url: fullUrl,
|
||||
method: options.method || 'GET',
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
responseStatus: error.response?.status,
|
||||
responseDataPreview: (() => {
|
||||
try {
|
||||
return JSON.stringify(error.response?.data).slice(0, 1000)
|
||||
} catch (_) {
|
||||
return String(error.response?.data || '')
|
||||
}
|
||||
})()
|
||||
})
|
||||
} catch (errLogErr) {
|
||||
console.warn('记录错误日志失败:', errLogErr)
|
||||
}
|
||||
|
||||
// 处理401错误
|
||||
if (error.response?.status === 401) {
|
||||
const errorCode = error.response?.data?.code
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,8 @@
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-input
|
||||
v-model:value="searchForm.farmer_name"
|
||||
placeholder="农户姓名"
|
||||
v-model:value="searchForm.policyholder_name"
|
||||
placeholder="投保人姓名"
|
||||
allow-clear
|
||||
/>
|
||||
</a-col>
|
||||
@@ -118,6 +118,9 @@
|
||||
<a-menu-item key="payment" v-if="record.payment_status === 'unpaid'">
|
||||
标记已支付
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete" style="color: #ff4d4f;">
|
||||
删除保单
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="link" size="small">
|
||||
@@ -146,20 +149,20 @@
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="农户姓名" name="farmer_name">
|
||||
<a-input v-model:value="formData.farmer_name" placeholder="请输入农户姓名" />
|
||||
<a-form-item label="投保人姓名" name="policyholder_name">
|
||||
<a-input v-model:value="formData.policyholder_name" placeholder="请输入投保人姓名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="农户电话" name="farmer_phone">
|
||||
<a-input v-model:value="formData.farmer_phone" placeholder="请输入农户电话" />
|
||||
<a-form-item label="投保人电话" name="policyholder_phone">
|
||||
<a-input v-model:value="formData.policyholder_phone" placeholder="请输入投保人电话" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="身份证号" name="farmer_id_card">
|
||||
<a-input v-model:value="formData.farmer_id_card" placeholder="请输入身份证号" />
|
||||
<a-form-item label="身份证号" name="policyholder_id_card">
|
||||
<a-input v-model:value="formData.policyholder_id_card" placeholder="请输入身份证号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
@@ -180,8 +183,26 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="农户地址" name="farmer_address">
|
||||
<a-textarea v-model:value="formData.farmer_address" placeholder="请输入农户地址" :rows="2" />
|
||||
<a-form-item label="投保人地址" name="policyholder_address">
|
||||
<a-textarea v-model:value="formData.policyholder_address" placeholder="请输入投保人地址" :rows="2" />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 养殖场信息 -->
|
||||
<a-divider orientation="left">养殖场信息</a-divider>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖场名称" name="farm_name">
|
||||
<a-input v-model:value="formData.farm_name" placeholder="请输入养殖场名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="养殖场许可证号" name="farm_license">
|
||||
<a-input v-model:value="formData.farm_license" placeholder="请输入养殖场许可证号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="养殖场地址" name="farm_address">
|
||||
<a-textarea v-model:value="formData.farm_address" placeholder="请输入养殖场地址" :rows="2" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
@@ -208,15 +229,11 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="保费费率" name="premium_rate">
|
||||
<a-input-number
|
||||
v-model:value="formData.premium_rate"
|
||||
placeholder="请输入费率"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:precision="4"
|
||||
style="width: 100%"
|
||||
@change="calculateAmounts"
|
||||
<a-form-item label="保费费率">
|
||||
<a-input
|
||||
:value="getCurrentPremiumRate()"
|
||||
disabled
|
||||
placeholder="自动计算"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -280,15 +297,18 @@
|
||||
{{ getStatusText(currentRecord.policy_status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="农户姓名">{{ currentRecord.farmer_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="农户电话">{{ currentRecord.farmer_phone }}</a-descriptions-item>
|
||||
<a-descriptions-item label="身份证号">{{ currentRecord.farmer_id_card }}</a-descriptions-item>
|
||||
<a-descriptions-item label="投保人姓名">{{ currentRecord.policyholder_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="投保人电话">{{ currentRecord.policyholder_phone }}</a-descriptions-item>
|
||||
<a-descriptions-item label="身份证号">{{ currentRecord.policyholder_id_card }}</a-descriptions-item>
|
||||
<a-descriptions-item label="牲畜类型">{{ currentRecord.livestock_type?.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="农户地址" :span="2">{{ currentRecord.farmer_address }}</a-descriptions-item>
|
||||
<a-descriptions-item label="投保人地址" :span="2">{{ currentRecord.policyholder_address }}</a-descriptions-item>
|
||||
<a-descriptions-item label="养殖场名称">{{ currentRecord.farm_name || '无' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="养殖场地址">{{ currentRecord.farm_address || '无' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="养殖场许可证号" :span="2">{{ currentRecord.farm_license || '无' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="牲畜数量">{{ currentRecord.livestock_count }}头</a-descriptions-item>
|
||||
<a-descriptions-item label="单头价值">¥{{ currentRecord.unit_value?.toLocaleString() }}</a-descriptions-item>
|
||||
<a-descriptions-item label="总保额">¥{{ currentRecord.total_value?.toLocaleString() }}</a-descriptions-item>
|
||||
<a-descriptions-item label="保费费率">{{ (currentRecord.premium_rate * 100).toFixed(2) }}%</a-descriptions-item>
|
||||
<a-descriptions-item label="保费费率">{{ currentRecord.livestock_type?.premium_rate ? (currentRecord.livestock_type.premium_rate * 100).toFixed(2) + '%' : '未知' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="保费金额">¥{{ currentRecord.premium_amount?.toLocaleString() }}</a-descriptions-item>
|
||||
<a-descriptions-item label="支付状态">
|
||||
<a-tag :color="getPaymentStatusColor(currentRecord.payment_status)">
|
||||
@@ -307,14 +327,14 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
DownOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { livestockPolicyApi } from '@/utils/api'
|
||||
import { livestockPolicyApi, livestockTypeApi } from '@/utils/api'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
@@ -328,7 +348,7 @@ const livestockTypes = ref([])
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
policy_no: '',
|
||||
farmer_name: '',
|
||||
policyholder_name: '',
|
||||
policy_status: undefined,
|
||||
payment_status: undefined,
|
||||
dateRange: []
|
||||
@@ -346,37 +366,43 @@ const pagination = reactive({
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
farmer_name: '',
|
||||
farmer_phone: '',
|
||||
farmer_id_card: '',
|
||||
farmer_address: '',
|
||||
policy_no: '',
|
||||
policyholder_name: '',
|
||||
policyholder_phone: '',
|
||||
policyholder_id_card: '',
|
||||
policyholder_address: '',
|
||||
farm_name: '',
|
||||
farm_address: '',
|
||||
farm_license: '',
|
||||
livestock_type_id: undefined,
|
||||
livestock_count: undefined,
|
||||
unit_value: undefined,
|
||||
premium_rate: undefined,
|
||||
total_value: 0,
|
||||
premium_amount: 0,
|
||||
start_date: undefined,
|
||||
end_date: undefined,
|
||||
policy_status: 'active',
|
||||
payment_status: 'unpaid',
|
||||
payment_date: undefined,
|
||||
policy_document_url: '',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
farmer_name: [{ required: true, message: '请输入农户姓名' }],
|
||||
farmer_phone: [
|
||||
{ required: true, message: '请输入农户电话' },
|
||||
policyholder_name: [{ required: true, message: '请输入投保人姓名' }],
|
||||
policyholder_phone: [
|
||||
{ required: true, message: '请输入投保人电话' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }
|
||||
],
|
||||
farmer_id_card: [
|
||||
policyholder_id_card: [
|
||||
{ required: true, message: '请输入身份证号' },
|
||||
{ pattern: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, message: '请输入正确的身份证号' }
|
||||
],
|
||||
farmer_address: [{ required: true, message: '请输入农户地址' }],
|
||||
policyholder_address: [{ required: true, message: '请输入投保人地址' }],
|
||||
livestock_type_id: [{ required: true, message: '请选择牲畜类型' }],
|
||||
livestock_count: [{ required: true, message: '请输入牲畜数量' }],
|
||||
unit_value: [{ required: true, message: '请输入单头价值' }],
|
||||
premium_rate: [{ required: true, message: '请输入保费费率' }],
|
||||
start_date: [{ required: true, message: '请选择保险开始日期' }],
|
||||
end_date: [{ required: true, message: '请选择保险结束日期' }]
|
||||
}
|
||||
@@ -395,15 +421,15 @@ const columns = [
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '农户姓名',
|
||||
dataIndex: 'farmer_name',
|
||||
key: 'farmer_name',
|
||||
title: '投保人姓名',
|
||||
dataIndex: 'policyholder_name',
|
||||
key: 'policyholder_name',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '农户电话',
|
||||
dataIndex: 'farmer_phone',
|
||||
key: 'farmer_phone',
|
||||
title: '投保人电话',
|
||||
dataIndex: 'policyholder_phone',
|
||||
key: 'policyholder_phone',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
@@ -500,12 +526,13 @@ const fetchData = async () => {
|
||||
|
||||
const fetchLivestockTypes = async () => {
|
||||
try {
|
||||
const response = await livestockPolicyApi.getLivestockTypes()
|
||||
const response = await livestockTypeApi.getList({ status: 'active' })
|
||||
if (response.code === 200) {
|
||||
livestockTypes.value = response.data.list
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取牲畜类型失败:', error)
|
||||
message.error('获取牲畜类型失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,6 +606,25 @@ const handleMenuClick = async ({ key }, record) => {
|
||||
message.success('支付状态已更新')
|
||||
fetchData()
|
||||
break
|
||||
case 'delete':
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除保单编号为 ${record.policy_no} 的保单吗?此操作不可恢复。`,
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await livestockPolicyApi.delete(record.id)
|
||||
message.success('保单删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
@@ -627,18 +673,24 @@ const resetForm = () => {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key === 'total_value' || key === 'premium_amount') {
|
||||
formData[key] = 0
|
||||
} else if (key === 'policy_status') {
|
||||
formData[key] = 'active'
|
||||
} else if (key === 'payment_status') {
|
||||
formData[key] = 'unpaid'
|
||||
} else if (key === 'notes' || key === 'policy_no' || key === 'policy_document_url') {
|
||||
formData[key] = ''
|
||||
} else {
|
||||
formData[key] = undefined
|
||||
}
|
||||
})
|
||||
formData.notes = ''
|
||||
}
|
||||
|
||||
const handleLivestockTypeChange = (value) => {
|
||||
const selectedType = livestockTypes.value.find(type => type.id === value)
|
||||
if (selectedType) {
|
||||
formData.unit_value = selectedType.base_value
|
||||
formData.premium_rate = selectedType.premium_rate
|
||||
// 设置单头价值的建议值(可以是最小值和最大值的中间值)
|
||||
const suggestedValue = (selectedType.unit_price_min + selectedType.unit_price_max) / 2
|
||||
formData.unit_value = suggestedValue
|
||||
calculateAmounts()
|
||||
}
|
||||
}
|
||||
@@ -647,8 +699,13 @@ const calculateAmounts = () => {
|
||||
if (formData.livestock_count && formData.unit_value) {
|
||||
formData.total_value = formData.livestock_count * formData.unit_value
|
||||
}
|
||||
if (formData.total_value && formData.premium_rate) {
|
||||
formData.premium_amount = formData.total_value * formData.premium_rate
|
||||
|
||||
// 从选中的牲畜类型获取保费费率
|
||||
if (formData.total_value && formData.livestock_type_id) {
|
||||
const selectedType = livestockTypes.value.find(type => type.id === formData.livestock_type_id)
|
||||
if (selectedType && selectedType.premium_rate) {
|
||||
formData.premium_amount = formData.total_value * selectedType.premium_rate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,6 +759,16 @@ const formatDateTime = (dateTime) => {
|
||||
return dateTime ? dayjs(dateTime).format('YYYY-MM-DD HH:mm:ss') : ''
|
||||
}
|
||||
|
||||
const getCurrentPremiumRate = () => {
|
||||
if (formData.livestock_type_id) {
|
||||
const selectedType = livestockTypes.value.find(type => type.id === formData.livestock_type_id)
|
||||
if (selectedType && selectedType.premium_rate) {
|
||||
return `${(selectedType.premium_rate * 100).toFixed(2)}%`
|
||||
}
|
||||
}
|
||||
return '请先选择牲畜类型'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
|
||||
@@ -192,6 +192,27 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="客户选择" name="customer_id">
|
||||
<a-select v-model:value="formState.customer_id" placeholder="请选择客户">
|
||||
<a-select-option :value="1">张三 (客户1)</a-select-option>
|
||||
<a-select-option :value="2">李四 (客户2)</a-select-option>
|
||||
<a-select-option :value="3">王五 (客户3)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="关联申请" name="application_id">
|
||||
<a-select v-model:value="formState.application_id" placeholder="请选择关联申请">
|
||||
<a-select-option :value="1">申请001</a-select-option>
|
||||
<a-select-option :value="2">申请002</a-select-option>
|
||||
<a-select-option :value="3">申请003</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="policyholder_name">
|
||||
@@ -295,8 +316,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
@@ -321,6 +343,8 @@ const searchForm = reactive({
|
||||
|
||||
const formState = reactive({
|
||||
policy_number: '',
|
||||
customer_id: null,
|
||||
application_id: null,
|
||||
insurance_type_id: null,
|
||||
policyholder_name: '',
|
||||
insured_name: '',
|
||||
@@ -527,8 +551,8 @@ const handleEdit = (record) => {
|
||||
insured_name: record.insured_name,
|
||||
premium_amount: record.premium_amount,
|
||||
coverage_amount: record.coverage_amount,
|
||||
start_date: record.start_date,
|
||||
end_date: record.end_date,
|
||||
start_date: record.start_date ? dayjs(record.start_date) : null,
|
||||
end_date: record.end_date ? dayjs(record.end_date) : null,
|
||||
status: record.status,
|
||||
phone: record.phone,
|
||||
email: record.email,
|
||||
@@ -542,18 +566,32 @@ const handleModalOk = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
// 准备提交数据,格式化日期
|
||||
const submitData = { ...formState }
|
||||
if (submitData.start_date) {
|
||||
submitData.start_date = submitData.start_date.format('YYYY-MM-DD')
|
||||
}
|
||||
if (submitData.end_date) {
|
||||
submitData.end_date = submitData.end_date.format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
if (editingId.value) {
|
||||
// await policyAPI.update(editingId.value, formState)
|
||||
await policyAPI.update(editingId.value, submitData)
|
||||
message.success('保单更新成功')
|
||||
} else {
|
||||
// await policyAPI.create(formState)
|
||||
await policyAPI.create(submitData)
|
||||
message.success('保单创建成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
loadPolicies()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败', error)
|
||||
if (error.errorFields) {
|
||||
console.log('表单验证失败', error)
|
||||
} else {
|
||||
message.error(editingId.value ? '保单更新失败' : '保单创建失败')
|
||||
console.error('API调用失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,11 +602,12 @@ const handleModalCancel = () => {
|
||||
const handleToggleStatus = async (record) => {
|
||||
try {
|
||||
const newStatus = record.status === 'active' ? 'cancelled' : 'active'
|
||||
// await policyAPI.update(record.id, { status: newStatus })
|
||||
await policyAPI.update(record.id, { status: newStatus })
|
||||
message.success('状态更新成功')
|
||||
loadPolicies()
|
||||
} catch (error) {
|
||||
message.error('状态更新失败')
|
||||
console.error('状态更新失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,13 +620,23 @@ const handleClaim = async (record) => {
|
||||
}
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
try {
|
||||
// await policyAPI.delete(id)
|
||||
message.success('保单删除成功')
|
||||
loadPolicies()
|
||||
} catch (error) {
|
||||
message.error('保单删除失败')
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个保单吗?此操作不可恢复。',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await policyAPI.delete(id)
|
||||
message.success('保单删除成功')
|
||||
loadPolicies()
|
||||
} catch (error) {
|
||||
message.error('保单删除失败')
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
|
||||
return {
|
||||
base: '/insurance/',
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
142
insurance_backend/check_insurance_page.js
Normal file
142
insurance_backend/check_insurance_page.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
async function checkInsuranceNavigation() {
|
||||
console.log('🔍 检查险种管理页面导航...');
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
slowMo: 1000
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
// 访问前端页面并登录
|
||||
await page.goto('http://localhost:3002', { waitUntil: 'networkidle2' });
|
||||
|
||||
// 登录
|
||||
await page.type('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]', 'admin');
|
||||
await page.type('input[type="password"], input[placeholder*="密码"]', '123456');
|
||||
|
||||
const loginSelectors = ['button[type="submit"]', '.login-btn', 'button'];
|
||||
for (const selector of loginSelectors) {
|
||||
try {
|
||||
const button = await page.$(selector);
|
||||
if (button) {
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试下一个选择器
|
||||
}
|
||||
}
|
||||
|
||||
// 等待登录完成
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
console.log('✅ 登录完成,当前URL:', page.url());
|
||||
|
||||
// 方法1:直接访问险种管理页面URL
|
||||
console.log('🔍 方法1:直接访问险种管理页面URL...');
|
||||
await page.goto('http://localhost:3002/#/insurance-types', { waitUntil: 'networkidle2' });
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
let currentUrl = page.url();
|
||||
console.log('直接访问后的URL:', currentUrl);
|
||||
|
||||
if (currentUrl.includes('insurance-types')) {
|
||||
console.log('✅ 直接访问险种管理页面成功');
|
||||
|
||||
// 检查页面内容
|
||||
const pageContent = await page.content();
|
||||
const hasInsuranceContent = pageContent.includes('险种管理') ||
|
||||
pageContent.includes('保险类型') ||
|
||||
pageContent.includes('新增险种');
|
||||
console.log('页面包含险种管理内容:', hasInsuranceContent);
|
||||
|
||||
// 查找表格元素
|
||||
const tableElements = await page.evaluate(() => {
|
||||
const selectors = [
|
||||
'table',
|
||||
'.ant-table',
|
||||
'.ant-table-tbody',
|
||||
'.el-table',
|
||||
'.data-table',
|
||||
'[class*="table"]',
|
||||
'tbody tr',
|
||||
'.ant-table-row'
|
||||
];
|
||||
|
||||
const found = [];
|
||||
selectors.forEach(selector => {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
if (elements.length > 0) {
|
||||
found.push({
|
||||
selector,
|
||||
count: elements.length,
|
||||
visible: elements[0] ? !elements[0].hidden && elements[0].offsetParent !== null : false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return found;
|
||||
});
|
||||
|
||||
console.log('找到的表格元素:', JSON.stringify(tableElements, null, 2));
|
||||
|
||||
// 检查是否有数据加载
|
||||
const hasData = await page.evaluate(() => {
|
||||
const rows = document.querySelectorAll('.ant-table-tbody tr, tbody tr, .table-row');
|
||||
return rows.length > 0;
|
||||
});
|
||||
|
||||
console.log('表格是否有数据:', hasData);
|
||||
|
||||
// 检查加载状态
|
||||
const isLoading = await page.evaluate(() => {
|
||||
const loadingElements = document.querySelectorAll('.ant-spin, .loading, [class*="loading"]');
|
||||
return loadingElements.length > 0;
|
||||
});
|
||||
|
||||
console.log('页面是否在加载中:', isLoading);
|
||||
|
||||
// 等待一段时间看数据是否会加载
|
||||
console.log('等待数据加载...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
const hasDataAfterWait = await page.evaluate(() => {
|
||||
const rows = document.querySelectorAll('.ant-table-tbody tr, tbody tr, .table-row');
|
||||
return rows.length > 0;
|
||||
});
|
||||
|
||||
console.log('等待后表格是否有数据:', hasDataAfterWait);
|
||||
|
||||
// 检查网络请求
|
||||
const networkLogs = [];
|
||||
page.on('response', response => {
|
||||
if (response.url().includes('insurance-types') || response.url().includes('api')) {
|
||||
networkLogs.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 刷新页面看网络请求
|
||||
await page.reload({ waitUntil: 'networkidle2' });
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
console.log('网络请求日志:', JSON.stringify(networkLogs, null, 2));
|
||||
|
||||
} else {
|
||||
console.log('❌ 直接访问险种管理页面失败,仍在:', currentUrl);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查导航失败:', error.message);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
checkInsuranceNavigation().catch(console.error);
|
||||
116
insurance_backend/check_login_dom.js
Normal file
116
insurance_backend/check_login_dom.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
async function checkLoginDOM() {
|
||||
console.log('🔍 检查登录后的DOM结构...');
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
slowMo: 500
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
// 访问前端页面
|
||||
await page.goto('http://localhost:3002', { waitUntil: 'networkidle2' });
|
||||
|
||||
// 填写登录信息
|
||||
await page.type('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]', 'admin');
|
||||
await page.type('input[type="password"], input[placeholder*="密码"]', '123456');
|
||||
|
||||
// 点击登录按钮
|
||||
const loginSelectors = ['button[type="submit"]', '.login-btn', 'button'];
|
||||
let loginClicked = false;
|
||||
|
||||
for (const selector of loginSelectors) {
|
||||
try {
|
||||
const button = await page.$(selector);
|
||||
if (button) {
|
||||
await button.click();
|
||||
loginClicked = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试下一个选择器
|
||||
}
|
||||
}
|
||||
|
||||
if (!loginClicked) {
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
// 等待页面加载
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// 获取当前URL
|
||||
const currentUrl = page.url();
|
||||
console.log('当前URL:', currentUrl);
|
||||
|
||||
// 检查页面标题
|
||||
const title = await page.title();
|
||||
console.log('页面标题:', title);
|
||||
|
||||
// 查找可能的用户信息元素
|
||||
const userElements = await page.evaluate(() => {
|
||||
const selectors = [
|
||||
'.user-info',
|
||||
'.header-user',
|
||||
'.logout-btn',
|
||||
'.user-name',
|
||||
'.username',
|
||||
'.user-avatar',
|
||||
'.header-right',
|
||||
'.navbar-user',
|
||||
'.el-dropdown',
|
||||
'[class*="user"]',
|
||||
'[class*="logout"]',
|
||||
'[class*="header"]'
|
||||
];
|
||||
|
||||
const found = [];
|
||||
selectors.forEach(selector => {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
if (elements.length > 0) {
|
||||
found.push({
|
||||
selector,
|
||||
count: elements.length,
|
||||
text: Array.from(elements).map(el => el.textContent?.trim()).filter(t => t)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return found;
|
||||
});
|
||||
|
||||
console.log('找到的用户相关元素:', JSON.stringify(userElements, null, 2));
|
||||
|
||||
// 获取页面的主要结构
|
||||
const pageStructure = await page.evaluate(() => {
|
||||
const body = document.body;
|
||||
const mainElements = [];
|
||||
|
||||
// 查找主要的容器元素
|
||||
const containers = body.querySelectorAll('div[class], nav[class], header[class], main[class]');
|
||||
containers.forEach(el => {
|
||||
if (el.className) {
|
||||
mainElements.push({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
className: el.className,
|
||||
text: el.textContent?.substring(0, 100) + '...'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return mainElements.slice(0, 20); // 只返回前20个
|
||||
});
|
||||
|
||||
console.log('页面主要结构:', JSON.stringify(pageStructure, null, 2));
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查DOM失败:', error.message);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
checkLoginDOM().catch(console.error);
|
||||
136
insurance_backend/check_token_status.js
Normal file
136
insurance_backend/check_token_status.js
Normal file
@@ -0,0 +1,136 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
async function checkTokenStatus() {
|
||||
console.log('🔍 检查Token状态...');
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
slowMo: 500
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
// 访问前端页面并登录
|
||||
await page.goto('http://localhost:3002', { waitUntil: 'networkidle2' });
|
||||
|
||||
// 登录
|
||||
await page.type('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]', 'admin');
|
||||
await page.type('input[type="password"], input[placeholder*="密码"]', '123456');
|
||||
|
||||
const loginSelectors = ['button[type="submit"]', '.login-btn', 'button'];
|
||||
for (const selector of loginSelectors) {
|
||||
try {
|
||||
const button = await page.$(selector);
|
||||
if (button) {
|
||||
await button.click();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试下一个选择器
|
||||
}
|
||||
}
|
||||
|
||||
// 等待登录完成
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
console.log('✅ 登录完成,当前URL:', page.url());
|
||||
|
||||
// 检查localStorage中的Token信息
|
||||
const tokenInfo = await page.evaluate(() => {
|
||||
return {
|
||||
accessToken: localStorage.getItem('accessToken'),
|
||||
refreshToken: localStorage.getItem('refreshToken'),
|
||||
tokenExpiresAt: localStorage.getItem('tokenExpiresAt'),
|
||||
userInfo: localStorage.getItem('userInfo'),
|
||||
currentTime: Date.now()
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Token信息:');
|
||||
console.log('- accessToken:', tokenInfo.accessToken ? `${tokenInfo.accessToken.substring(0, 20)}...` : 'null');
|
||||
console.log('- refreshToken:', tokenInfo.refreshToken ? `${tokenInfo.refreshToken.substring(0, 20)}...` : 'null');
|
||||
console.log('- tokenExpiresAt:', tokenInfo.tokenExpiresAt);
|
||||
console.log('- userInfo:', tokenInfo.userInfo);
|
||||
console.log('- currentTime:', tokenInfo.currentTime);
|
||||
|
||||
if (tokenInfo.tokenExpiresAt) {
|
||||
const expiresAt = parseInt(tokenInfo.tokenExpiresAt);
|
||||
const isExpired = tokenInfo.currentTime >= expiresAt;
|
||||
const timeUntilExpiry = expiresAt - tokenInfo.currentTime;
|
||||
|
||||
console.log('- Token是否过期:', isExpired);
|
||||
console.log('- 距离过期时间:', Math.round(timeUntilExpiry / 1000), '秒');
|
||||
}
|
||||
|
||||
// 测试API调用
|
||||
console.log('🔍 测试API调用...');
|
||||
const apiResponse = await page.evaluate(async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const response = await fetch('http://localhost:3000/api/insurance-types', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
data: response.ok ? await response.json() : await response.text()
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
console.log('API响应:', JSON.stringify(apiResponse, null, 2));
|
||||
|
||||
// 现在尝试访问险种管理页面
|
||||
console.log('🔍 尝试访问险种管理页面...');
|
||||
await page.goto('http://localhost:3002/#/insurance-types', { waitUntil: 'networkidle2' });
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
const finalUrl = page.url();
|
||||
console.log('最终URL:', finalUrl);
|
||||
|
||||
if (finalUrl.includes('insurance-types')) {
|
||||
console.log('✅ 成功访问险种管理页面');
|
||||
|
||||
// 检查页面是否有表格
|
||||
const hasTable = await page.evaluate(() => {
|
||||
const tables = document.querySelectorAll('table, .ant-table, .ant-table-tbody');
|
||||
return tables.length > 0;
|
||||
});
|
||||
|
||||
console.log('页面是否有表格:', hasTable);
|
||||
|
||||
} else if (finalUrl.includes('login')) {
|
||||
console.log('❌ 被重定向到登录页面');
|
||||
|
||||
// 检查控制台错误
|
||||
const consoleLogs = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
consoleLogs.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
console.log('控制台错误:', consoleLogs);
|
||||
|
||||
} else {
|
||||
console.log('❌ 访问了其他页面:', finalUrl);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查Token状态失败:', error.message);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
checkTokenStatus().catch(console.error);
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"development": {
|
||||
"username": "root",
|
||||
"password": "aiotAiot123!",
|
||||
"database": "insurance_data",
|
||||
"host": "129.211.213.226",
|
||||
"port": 9527,
|
||||
"dialect": "mysql",
|
||||
"timezone": "+08:00",
|
||||
"dialectOptions": {
|
||||
"dateStrings": true,
|
||||
"typeCast": true
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"username": "root",
|
||||
"password": "aiotAiot123!",
|
||||
"database": "insurance_data_test",
|
||||
"host": "129.211.213.226",
|
||||
"port": 9527,
|
||||
"dialect": "mysql",
|
||||
"timezone": "+08:00"
|
||||
},
|
||||
"production": {
|
||||
"username": "root",
|
||||
"password": "aiotAiot123!",
|
||||
"database": "insurance_data_prod",
|
||||
"host": "129.211.213.226",
|
||||
"port": 9527,
|
||||
"dialect": "mysql",
|
||||
"timezone": "+08:00"
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
require('dotenv').config();
|
||||
|
||||
// 创建Sequelize实例
|
||||
const sequelize = new Sequelize({
|
||||
dialect: process.env.DB_DIALECT || 'mysql',
|
||||
host: process.env.DB_HOST || '129.211.213.226',
|
||||
port: process.env.DB_PORT || 9527,
|
||||
database: process.env.DB_DATABASE || 'insurance_data',
|
||||
username: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || 'aiotAiot123!',
|
||||
logging: process.env.NODE_ENV === 'development' ? console.log : false,
|
||||
pool: {
|
||||
max: 10,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
},
|
||||
define: {
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci'
|
||||
},
|
||||
dialectOptions: {
|
||||
// 解决MySQL严格模式问题
|
||||
dateStrings: true,
|
||||
typeCast: true,
|
||||
// 允许0000-00-00日期值
|
||||
connectAttributes: {
|
||||
sql_mode: 'TRADITIONAL'
|
||||
}
|
||||
},
|
||||
timezone: '+08:00' // 设置时区为东八区
|
||||
});
|
||||
|
||||
// 测试数据库连接
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { sequelize, testConnection };
|
||||
@@ -1,42 +0,0 @@
|
||||
const redis = require('redis');
|
||||
require('dotenv').config();
|
||||
|
||||
// 创建Redis客户端
|
||||
const createRedisClient = () => {
|
||||
const client = redis.createClient({
|
||||
socket: {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379
|
||||
},
|
||||
password: process.env.REDIS_PASSWORD || '',
|
||||
legacyMode: false
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
client.on('error', (err) => {
|
||||
console.error('❌ Redis连接错误:', err);
|
||||
});
|
||||
|
||||
// 连接成功
|
||||
client.on('connect', () => {
|
||||
console.log('✅ Redis连接成功');
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
// 创建并连接Redis客户端
|
||||
const redisClient = createRedisClient();
|
||||
|
||||
// 连接Redis
|
||||
const connectRedis = async () => {
|
||||
try {
|
||||
await redisClient.connect();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Redis连接失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { redisClient, connectRedis };
|
||||
@@ -1,244 +0,0 @@
|
||||
const { Claim, Policy, User, InsuranceApplication, InsuranceType } = require('../models');
|
||||
const responseFormat = require('../utils/response');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 获取理赔列表
|
||||
const getClaims = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
claim_no,
|
||||
customer_name,
|
||||
claim_status,
|
||||
dateRange,
|
||||
page = 1,
|
||||
limit = 10
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
|
||||
// 理赔编号筛选
|
||||
if (claim_no) {
|
||||
whereClause.claim_no = { [Op.like]: `%${claim_no}%` };
|
||||
}
|
||||
|
||||
// 理赔状态筛选
|
||||
if (claim_status) {
|
||||
whereClause.claim_status = claim_status;
|
||||
}
|
||||
|
||||
// 日期范围筛选
|
||||
if (dateRange && dateRange.start && dateRange.end) {
|
||||
whereClause.claim_date = {
|
||||
[Op.between]: [new Date(dateRange.start), new Date(dateRange.end)]
|
||||
};
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await Claim.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{
|
||||
model: Policy,
|
||||
as: 'policy',
|
||||
attributes: ['id', 'policy_no', 'coverage_amount'],
|
||||
include: [
|
||||
{
|
||||
model: InsuranceApplication,
|
||||
as: 'application',
|
||||
attributes: ['id', 'customer_name'],
|
||||
include: [
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insurance_type',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'customer',
|
||||
attributes: ['id', 'real_name', 'username']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'reviewer',
|
||||
attributes: ['id', 'real_name', 'username']
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
offset,
|
||||
limit: parseInt(limit)
|
||||
});
|
||||
|
||||
res.json(responseFormat.pagination(rows, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: count
|
||||
}, '获取理赔列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取理赔列表错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取理赔列表失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 创建理赔申请
|
||||
const createClaim = async (req, res) => {
|
||||
try {
|
||||
const claimData = req.body;
|
||||
|
||||
// 生成理赔编号
|
||||
const claimNo = `CLM${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
||||
|
||||
const claim = await Claim.create({
|
||||
...claimData,
|
||||
claim_no: claimNo
|
||||
});
|
||||
|
||||
res.status(201).json(responseFormat.created(claim, '理赔申请创建成功'));
|
||||
} catch (error) {
|
||||
console.error('创建理赔申请错误:', error);
|
||||
res.status(500).json(responseFormat.error('创建理赔申请失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取单个理赔详情
|
||||
const getClaimById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const claim = await Claim.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: Policy,
|
||||
as: 'policy',
|
||||
include: [
|
||||
{
|
||||
model: InsuranceApplication,
|
||||
as: 'application',
|
||||
include: [
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insurance_type',
|
||||
attributes: ['id', 'name', 'description']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'customer',
|
||||
attributes: ['id', 'real_name', 'username', 'phone', 'email']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'reviewer',
|
||||
attributes: ['id', 'real_name', 'username']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!claim) {
|
||||
return res.status(404).json(responseFormat.error('理赔申请不存在'));
|
||||
}
|
||||
|
||||
res.json(responseFormat.success(claim, '获取理赔详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取理赔详情错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取理赔详情失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 审核理赔申请
|
||||
const reviewClaim = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { claim_status, review_notes } = req.body;
|
||||
const reviewerId = req.user.id;
|
||||
|
||||
const claim = await Claim.findByPk(id);
|
||||
if (!claim) {
|
||||
return res.status(404).json(responseFormat.error('理赔申请不存在'));
|
||||
}
|
||||
|
||||
if (!['approved', 'rejected', 'processing', 'paid'].includes(claim_status)) {
|
||||
return res.status(400).json(responseFormat.error('无效的理赔状态'));
|
||||
}
|
||||
|
||||
await claim.update({
|
||||
claim_status,
|
||||
review_notes,
|
||||
reviewer_id: reviewerId,
|
||||
review_date: new Date()
|
||||
});
|
||||
|
||||
res.json(responseFormat.success(claim, '理赔申请审核成功'));
|
||||
} catch (error) {
|
||||
console.error('审核理赔申请错误:', error);
|
||||
res.status(500).json(responseFormat.error('审核理赔申请失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新理赔支付状态
|
||||
const updateClaimPayment = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const claim = await Claim.findByPk(id);
|
||||
if (!claim) {
|
||||
return res.status(404).json(responseFormat.error('理赔申请不存在'));
|
||||
}
|
||||
|
||||
if (claim.claim_status !== 'approved') {
|
||||
return res.status(400).json(responseFormat.error('只有已批准的理赔才能进行支付'));
|
||||
}
|
||||
|
||||
await claim.update({
|
||||
claim_status: 'paid',
|
||||
payment_date: new Date()
|
||||
});
|
||||
|
||||
res.json(responseFormat.success(claim, '理赔支付状态更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新理赔支付状态错误:', error);
|
||||
res.status(500).json(responseFormat.error('更新理赔支付状态失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取理赔统计
|
||||
const getClaimStats = async (req, res) => {
|
||||
try {
|
||||
const stats = await Claim.findAll({
|
||||
attributes: [
|
||||
'claim_status',
|
||||
[Claim.sequelize.fn('COUNT', Claim.sequelize.col('id')), 'count'],
|
||||
[Claim.sequelize.fn('SUM', Claim.sequelize.col('claim_amount')), 'total_amount']
|
||||
],
|
||||
group: ['claim_status']
|
||||
});
|
||||
|
||||
const total = await Claim.count();
|
||||
const totalAmount = await Claim.sum('claim_amount');
|
||||
|
||||
res.json(responseFormat.success({
|
||||
stats,
|
||||
total,
|
||||
total_amount: totalAmount || 0
|
||||
}, '获取理赔统计成功'));
|
||||
} catch (error) {
|
||||
console.error('获取理赔统计错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取理赔统计失败'));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getClaims,
|
||||
createClaim,
|
||||
getClaimById,
|
||||
reviewClaim,
|
||||
updateClaimPayment,
|
||||
getClaimStats
|
||||
};
|
||||
@@ -1,435 +0,0 @@
|
||||
const { Device, DeviceAlert, User } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* 设备控制器
|
||||
*/
|
||||
class DeviceController {
|
||||
|
||||
/**
|
||||
* 获取设备列表
|
||||
*/
|
||||
static async getDeviceList(req, res) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
device_type,
|
||||
status,
|
||||
farm_id,
|
||||
pen_id,
|
||||
keyword
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const whereCondition = {};
|
||||
if (device_type) {
|
||||
whereCondition.device_type = device_type;
|
||||
}
|
||||
if (status) {
|
||||
whereCondition.status = status;
|
||||
}
|
||||
if (farm_id) {
|
||||
whereCondition.farm_id = farm_id;
|
||||
}
|
||||
if (pen_id) {
|
||||
whereCondition.pen_id = pen_id;
|
||||
}
|
||||
if (keyword) {
|
||||
whereCondition[Op.or] = [
|
||||
{ device_code: { [Op.like]: `%${keyword}%` } },
|
||||
{ device_name: { [Op.like]: `%${keyword}%` } },
|
||||
{ device_model: { [Op.like]: `%${keyword}%` } },
|
||||
{ manufacturer: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await Device.findAndCountAll({
|
||||
where: whereCondition,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'creator',
|
||||
attributes: ['id', 'username', 'real_name'],
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: offset
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
devices: rows,
|
||||
pagination: {
|
||||
current_page: parseInt(page),
|
||||
per_page: parseInt(limit),
|
||||
total: count,
|
||||
total_pages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取设备列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取设备列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备详情
|
||||
*/
|
||||
static async getDeviceDetail(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const device = await Device.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'creator',
|
||||
attributes: ['id', 'username', 'real_name'],
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'updater',
|
||||
attributes: ['id', 'username', 'real_name'],
|
||||
required: false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!device) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '设备不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取设备相关的预警统计
|
||||
const alertStats = await DeviceAlert.findAll({
|
||||
attributes: [
|
||||
'alert_level',
|
||||
[DeviceAlert.sequelize.fn('COUNT', DeviceAlert.sequelize.col('id')), 'count']
|
||||
],
|
||||
where: {
|
||||
device_id: id
|
||||
},
|
||||
group: ['alert_level'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 获取最近的预警记录
|
||||
const recentAlerts = await DeviceAlert.findAll({
|
||||
where: {
|
||||
device_id: id
|
||||
},
|
||||
order: [['alert_time', 'DESC']],
|
||||
limit: 5,
|
||||
attributes: ['id', 'alert_type', 'alert_level', 'alert_title', 'alert_time', 'status']
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
device,
|
||||
alert_stats: alertStats,
|
||||
recent_alerts: recentAlerts
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取设备详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取设备详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建设备
|
||||
*/
|
||||
static async createDevice(req, res) {
|
||||
try {
|
||||
const {
|
||||
device_code,
|
||||
device_name,
|
||||
device_type,
|
||||
device_model,
|
||||
manufacturer,
|
||||
installation_location,
|
||||
installation_date,
|
||||
farm_id,
|
||||
pen_id,
|
||||
status = 'normal'
|
||||
} = req.body;
|
||||
|
||||
const userId = req.user.id;
|
||||
|
||||
// 检查设备编号是否已存在
|
||||
const existingDevice = await Device.findOne({
|
||||
where: { device_code }
|
||||
});
|
||||
|
||||
if (existingDevice) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '设备编号已存在'
|
||||
});
|
||||
}
|
||||
|
||||
const device = await Device.create({
|
||||
device_code,
|
||||
device_name,
|
||||
device_type,
|
||||
device_model,
|
||||
manufacturer,
|
||||
installation_location,
|
||||
installation_date,
|
||||
farm_id,
|
||||
pen_id,
|
||||
status,
|
||||
created_by: userId,
|
||||
updated_by: userId
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '设备创建成功',
|
||||
data: device
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('创建设备失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建设备失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备
|
||||
*/
|
||||
static async updateDevice(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
device_code,
|
||||
device_name,
|
||||
device_type,
|
||||
device_model,
|
||||
manufacturer,
|
||||
installation_location,
|
||||
installation_date,
|
||||
farm_id,
|
||||
pen_id,
|
||||
status
|
||||
} = req.body;
|
||||
|
||||
const userId = req.user.id;
|
||||
|
||||
const device = await Device.findByPk(id);
|
||||
if (!device) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '设备不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 如果修改了设备编号,检查是否与其他设备重复
|
||||
if (device_code && device_code !== device.device_code) {
|
||||
const existingDevice = await Device.findOne({
|
||||
where: {
|
||||
device_code,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingDevice) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '设备编号已存在'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await device.update({
|
||||
device_code,
|
||||
device_name,
|
||||
device_type,
|
||||
device_model,
|
||||
manufacturer,
|
||||
installation_location,
|
||||
installation_date,
|
||||
farm_id,
|
||||
pen_id,
|
||||
status,
|
||||
updated_by: userId
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '设备更新成功',
|
||||
data: device
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('更新设备失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新设备失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除设备
|
||||
*/
|
||||
static async deleteDevice(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const device = await Device.findByPk(id);
|
||||
if (!device) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '设备不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否有关联的预警记录
|
||||
const alertCount = await DeviceAlert.count({
|
||||
where: { device_id: id }
|
||||
});
|
||||
|
||||
if (alertCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '该设备存在预警记录,无法删除'
|
||||
});
|
||||
}
|
||||
|
||||
await device.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '设备删除成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('删除设备失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除设备失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备类型列表
|
||||
*/
|
||||
static async getDeviceTypes(req, res) {
|
||||
try {
|
||||
const deviceTypes = await Device.findAll({
|
||||
attributes: [
|
||||
[Device.sequelize.fn('DISTINCT', Device.sequelize.col('device_type')), 'device_type']
|
||||
],
|
||||
where: {
|
||||
device_type: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
},
|
||||
raw: true
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: deviceTypes.map(item => item.device_type)
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取设备类型失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取设备类型失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备状态统计
|
||||
*/
|
||||
static async getDeviceStats(req, res) {
|
||||
try {
|
||||
const { farm_id } = req.query;
|
||||
|
||||
const whereCondition = {};
|
||||
if (farm_id) {
|
||||
whereCondition.farm_id = farm_id;
|
||||
}
|
||||
|
||||
// 按状态统计
|
||||
const devicesByStatus = await Device.findAll({
|
||||
attributes: [
|
||||
'status',
|
||||
[Device.sequelize.fn('COUNT', Device.sequelize.col('id')), 'count']
|
||||
],
|
||||
where: whereCondition,
|
||||
group: ['status'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 按类型统计
|
||||
const devicesByType = await Device.findAll({
|
||||
attributes: [
|
||||
'device_type',
|
||||
[Device.sequelize.fn('COUNT', Device.sequelize.col('id')), 'count']
|
||||
],
|
||||
where: whereCondition,
|
||||
group: ['device_type'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 总设备数
|
||||
const totalDevices = await Device.count({
|
||||
where: whereCondition
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total_devices: totalDevices,
|
||||
devices_by_status: devicesByStatus,
|
||||
devices_by_type: devicesByType
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取设备统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取设备统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeviceController;
|
||||
@@ -1,477 +0,0 @@
|
||||
const InstallationTask = require('../models/InstallationTask');
|
||||
const User = require('../models/User');
|
||||
const { Op, sequelize } = require('sequelize');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class InstallationTaskController {
|
||||
|
||||
// 获取待安装任务列表
|
||||
async getInstallationTasks(req, res) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
policyNumber,
|
||||
customerName,
|
||||
installationStatus,
|
||||
priority,
|
||||
keyword,
|
||||
startDate,
|
||||
endDate
|
||||
} = req.query;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
if (policyNumber) {
|
||||
whereConditions.policyNumber = { [Op.like]: `%${policyNumber}%` };
|
||||
}
|
||||
|
||||
if (customerName) {
|
||||
whereConditions.customerName = { [Op.like]: `%${customerName}%` };
|
||||
}
|
||||
|
||||
if (installationStatus) {
|
||||
whereConditions.installationStatus = installationStatus;
|
||||
}
|
||||
|
||||
if (priority) {
|
||||
whereConditions.priority = priority;
|
||||
}
|
||||
|
||||
// 关键字搜索(搜索申请单号、保单编号、客户姓名等)
|
||||
if (keyword) {
|
||||
whereConditions[Op.or] = [
|
||||
{ applicationNumber: { [Op.like]: `%${keyword}%` } },
|
||||
{ policyNumber: { [Op.like]: `%${keyword}%` } },
|
||||
{ customerName: { [Op.like]: `%${customerName}%` } },
|
||||
{ productName: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
whereConditions.taskGeneratedTime = {
|
||||
[Op.between]: [new Date(startDate), new Date(endDate)]
|
||||
};
|
||||
}
|
||||
|
||||
const { count, rows } = await InstallationTask.findAndCountAll({
|
||||
where: whereConditions,
|
||||
order: [['taskGeneratedTime', 'DESC']],
|
||||
offset,
|
||||
limit
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: '获取待安装任务列表成功',
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: limit,
|
||||
totalPages: Math.ceil(count / limit)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取待安装任务列表失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '获取待安装任务列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建待安装任务
|
||||
async createInstallationTask(req, res) {
|
||||
try {
|
||||
const {
|
||||
applicationNumber,
|
||||
policyNumber,
|
||||
productName,
|
||||
customerName,
|
||||
idType,
|
||||
idNumber,
|
||||
livestockSupplyType,
|
||||
pendingDevices,
|
||||
installationStatus = '待安装',
|
||||
priority = '中',
|
||||
assignedTo,
|
||||
taskGeneratedTime = new Date()
|
||||
} = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!applicationNumber || !policyNumber || !productName || !customerName) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
status: 'error',
|
||||
message: '申请单号、保单编号、产品名称、客户姓名为必填项'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查申请单号是否已存在
|
||||
const existingTask = await InstallationTask.findOne({
|
||||
where: { applicationNumber }
|
||||
});
|
||||
|
||||
if (existingTask) {
|
||||
return res.status(409).json({
|
||||
code: 409,
|
||||
status: 'error',
|
||||
message: '该申请单号已存在待安装任务'
|
||||
});
|
||||
}
|
||||
|
||||
const installationTask = await InstallationTask.create({
|
||||
applicationNumber,
|
||||
policyNumber,
|
||||
productName,
|
||||
customerName,
|
||||
idType: idType || '身份证',
|
||||
idNumber: idNumber || '',
|
||||
livestockSupplyType,
|
||||
pendingDevices: pendingDevices ? JSON.stringify(pendingDevices) : null,
|
||||
installationStatus,
|
||||
priority,
|
||||
assignedTo,
|
||||
taskGeneratedTime,
|
||||
createdBy: req.user?.id
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
code: 201,
|
||||
status: 'success',
|
||||
message: '创建待安装任务成功',
|
||||
data: installationTask
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('创建待安装任务失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '创建待安装任务失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取待安装任务详情
|
||||
async getInstallationTaskById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const installationTask = await InstallationTask.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'technician',
|
||||
attributes: ['id', 'username', 'real_name']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'creator',
|
||||
attributes: ['id', 'username', 'real_name']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'updater',
|
||||
attributes: ['id', 'username', 'real_name']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!installationTask) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
status: 'error',
|
||||
message: '待安装任务不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: '获取待安装任务详情成功',
|
||||
data: installationTask
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取待安装任务详情失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '获取待安装任务详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新待安装任务
|
||||
async updateInstallationTask(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = { ...req.body };
|
||||
|
||||
// 添加更新人信息
|
||||
updateData.updatedBy = req.user?.id;
|
||||
|
||||
// 处理设备数据
|
||||
if (updateData.pendingDevices) {
|
||||
updateData.pendingDevices = JSON.stringify(updateData.pendingDevices);
|
||||
}
|
||||
|
||||
// 处理安装完成时间
|
||||
if (updateData.installationStatus === '已安装' && !updateData.installationCompletedTime) {
|
||||
updateData.installationCompletedTime = new Date();
|
||||
}
|
||||
|
||||
const [updatedCount] = await InstallationTask.update(updateData, {
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (updatedCount === 0) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
status: 'error',
|
||||
message: '待安装任务不存在或未做任何修改'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取更新后的任务
|
||||
const updatedTask = await InstallationTask.findByPk(id);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: '更新待安装任务成功',
|
||||
data: updatedTask
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('更新待安装任务失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '更新待安装任务失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 删除待安装任务
|
||||
async deleteInstallationTask(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const deletedCount = await InstallationTask.destroy({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
if (deletedCount === 0) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
status: 'error',
|
||||
message: '待安装任务不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: '删除待安装任务成功'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('删除待安装任务失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '删除待安装任务失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 批量操作待安装任务
|
||||
async batchOperateInstallationTasks(req, res) {
|
||||
try {
|
||||
const { ids, operation, data } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
status: 'error',
|
||||
message: '任务ID列表不能为空'
|
||||
});
|
||||
}
|
||||
|
||||
let result;
|
||||
switch (operation) {
|
||||
case 'assign':
|
||||
if (!data.assignedTo) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
status: 'error',
|
||||
message: '分配操作需要指定分配给的用户'
|
||||
});
|
||||
}
|
||||
result = await InstallationTask.update(
|
||||
{ assignedTo: data.assignedTo, updatedBy: req.user?.id },
|
||||
{ where: { id: ids } }
|
||||
);
|
||||
break;
|
||||
case 'updateStatus':
|
||||
if (!data.installationStatus) {
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
status: 'error',
|
||||
message: '状态更新操作需要指定新状态'
|
||||
});
|
||||
}
|
||||
result = await InstallationTask.update(
|
||||
{ installationStatus: data.installationStatus, updatedBy: req.user?.id },
|
||||
{ where: { id: ids } }
|
||||
);
|
||||
break;
|
||||
case 'delete':
|
||||
result = await InstallationTask.destroy({ where: { id: ids } });
|
||||
break;
|
||||
default:
|
||||
return res.status(400).json({
|
||||
code: 400,
|
||||
status: 'error',
|
||||
message: '不支持的操作类型'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: `批量${operation}操作完成`,
|
||||
data: { affectedRows: result[0] || result }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('批量操作待安装任务失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '批量操作失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出待安装任务数据
|
||||
async exportInstallationTasks(req, res) {
|
||||
try {
|
||||
const { ids } = req.query;
|
||||
let where = {};
|
||||
|
||||
if (ids) {
|
||||
where.id = { [Op.in]: ids.split(',') };
|
||||
}
|
||||
|
||||
const tasks = await InstallationTask.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'technician',
|
||||
attributes: ['username', 'real_name']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'creator',
|
||||
attributes: ['username', 'real_name']
|
||||
}
|
||||
],
|
||||
order: [['taskGeneratedTime', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: '导出待安装任务数据成功',
|
||||
data: tasks
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('导出待安装任务数据失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '导出数据失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务统计信息
|
||||
async getInstallationTaskStats(req, res) {
|
||||
try {
|
||||
// 按状态统计
|
||||
const statusStats = await InstallationTask.findAll({
|
||||
attributes: [
|
||||
'installationStatus',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['installationStatus'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 按优先级统计
|
||||
const priorityStats = await InstallationTask.findAll({
|
||||
attributes: [
|
||||
'priority',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['priority'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 总数统计
|
||||
const total = await InstallationTask.count();
|
||||
|
||||
// 本月新增任务
|
||||
const thisMonth = await InstallationTask.count({
|
||||
where: {
|
||||
taskGeneratedTime: {
|
||||
[Op.gte]: new Date(new Date().getFullYear(), new Date().getMonth(), 1)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: '获取任务统计信息成功',
|
||||
data: {
|
||||
total,
|
||||
thisMonth,
|
||||
statusStats,
|
||||
priorityStats
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('获取任务统计信息失败:', error);
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '获取统计信息失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new InstallationTaskController();
|
||||
@@ -1,400 +0,0 @@
|
||||
const { InsuranceApplication, InsuranceType, User } = require('../models');
|
||||
const responseFormat = require('../utils/response');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 获取保险申请列表
|
||||
const getApplications = async (req, res) => {
|
||||
try {
|
||||
console.log('获取保险申请列表 - 请求参数:', req.query);
|
||||
const {
|
||||
applicantName,
|
||||
status,
|
||||
insuranceType,
|
||||
insuranceCategory,
|
||||
applicationNumber,
|
||||
dateRange,
|
||||
page = 1,
|
||||
limit = 10
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
const includeClause = [
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insurance_type',
|
||||
attributes: ['id', 'name', 'description'],
|
||||
where: {}
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'reviewer',
|
||||
attributes: ['id', 'real_name', 'username']
|
||||
}
|
||||
];
|
||||
|
||||
// 申请单号筛选
|
||||
if (applicationNumber) {
|
||||
whereClause.application_no = { [Op.like]: `%${applicationNumber}%` };
|
||||
}
|
||||
|
||||
// 投保人姓名筛选
|
||||
if (applicantName) {
|
||||
whereClause.customer_name = { [Op.like]: `%${applicantName}%` };
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
|
||||
// 参保险种筛选
|
||||
if (insuranceType) {
|
||||
includeClause[0].where.name = { [Op.like]: `%${insuranceType}%` };
|
||||
}
|
||||
|
||||
// 参保类型筛选
|
||||
if (insuranceCategory) {
|
||||
whereClause.insurance_category = { [Op.like]: `%${insuranceCategory}%` };
|
||||
}
|
||||
|
||||
// 日期范围筛选
|
||||
if (dateRange && dateRange.start && dateRange.end) {
|
||||
whereClause.application_date = {
|
||||
[Op.between]: [new Date(dateRange.start), new Date(dateRange.end)]
|
||||
};
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 如果没有保险类型筛选条件,清空where条件
|
||||
if (!insuranceType) {
|
||||
delete includeClause[0].where;
|
||||
}
|
||||
|
||||
console.log('查询条件:', { whereClause, includeClause, offset, limit: parseInt(limit) });
|
||||
|
||||
const { count, rows } = await InsuranceApplication.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: includeClause,
|
||||
order: [['created_at', 'DESC']],
|
||||
offset,
|
||||
limit: parseInt(limit)
|
||||
});
|
||||
|
||||
console.log('查询结果:', { count, rowsLength: rows.length });
|
||||
|
||||
res.json(responseFormat.pagination(rows, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: count
|
||||
}, '获取保险申请列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取保险申请列表错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取保险申请列表失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 创建保险申请
|
||||
const createApplication = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
customer_name,
|
||||
customer_id_card,
|
||||
customer_phone,
|
||||
customer_address,
|
||||
insurance_type_id,
|
||||
insurance_category,
|
||||
application_quantity,
|
||||
application_amount,
|
||||
remarks
|
||||
} = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!customer_name || !customer_id_card || !customer_phone || !customer_address || !insurance_type_id) {
|
||||
return res.status(400).json(responseFormat.error('请填写所有必填字段'));
|
||||
}
|
||||
|
||||
// 生成申请编号
|
||||
const applicationNo = `${new Date().getFullYear()}${(new Date().getMonth() + 1).toString().padStart(2, '0')}${new Date().getDate().toString().padStart(2, '0')}${Date.now().toString().slice(-6)}`;
|
||||
|
||||
const application = await InsuranceApplication.create({
|
||||
application_no: applicationNo,
|
||||
customer_name,
|
||||
customer_id_card,
|
||||
customer_phone,
|
||||
customer_address,
|
||||
insurance_type_id,
|
||||
insurance_category,
|
||||
application_quantity: application_quantity || 1,
|
||||
application_amount: application_amount || 0,
|
||||
remarks,
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
// 返回创建的申请信息,包含关联数据
|
||||
const createdApplication = await InsuranceApplication.findByPk(application.id, {
|
||||
include: [
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insurance_type',
|
||||
attributes: ['id', 'name', 'description']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
res.status(201).json(responseFormat.created(createdApplication, '保险申请创建成功'));
|
||||
} catch (error) {
|
||||
console.error('创建保险申请错误:', error);
|
||||
if (error.name === 'SequelizeValidationError') {
|
||||
const errorMessages = error.errors.map(err => err.message).join(', ');
|
||||
return res.status(400).json(responseFormat.error(`数据验证失败: ${errorMessages}`));
|
||||
}
|
||||
res.status(500).json(responseFormat.error('创建保险申请失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取单个保险申请详情
|
||||
const getApplicationById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const application = await InsuranceApplication.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insurance_type',
|
||||
attributes: ['id', 'name', 'description']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'reviewer',
|
||||
attributes: ['id', 'real_name', 'username']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!application) {
|
||||
return res.status(404).json(responseFormat.error('保险申请不存在'));
|
||||
}
|
||||
|
||||
res.json(responseFormat.success(application, '获取保险申请详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取保险申请详情错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取保险申请详情失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新保险申请
|
||||
const updateApplication = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const application = await InsuranceApplication.findByPk(id);
|
||||
if (!application) {
|
||||
return res.status(404).json(responseFormat.error('保险申请不存在'));
|
||||
}
|
||||
|
||||
await application.update(updateData);
|
||||
|
||||
res.json(responseFormat.success(application, '保险申请更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新保险申请错误:', error);
|
||||
res.status(500).json(responseFormat.error('更新保险申请失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 审核保险申请
|
||||
const reviewApplication = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, review_notes } = req.body;
|
||||
const reviewerId = req.user.id;
|
||||
|
||||
const application = await InsuranceApplication.findByPk(id);
|
||||
if (!application) {
|
||||
return res.status(404).json(responseFormat.error('保险申请不存在'));
|
||||
}
|
||||
|
||||
if (!['approved', 'rejected', 'under_review'].includes(status)) {
|
||||
return res.status(400).json(responseFormat.error('无效的审核状态'));
|
||||
}
|
||||
|
||||
await application.update({
|
||||
status,
|
||||
review_notes,
|
||||
reviewer_id: reviewerId,
|
||||
review_date: new Date()
|
||||
});
|
||||
|
||||
res.json(responseFormat.success(application, '保险申请审核成功'));
|
||||
} catch (error) {
|
||||
console.error('审核保险申请错误:', error);
|
||||
res.status(500).json(responseFormat.error('审核保险申请失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除保险申请
|
||||
const deleteApplication = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const application = await InsuranceApplication.findByPk(id);
|
||||
if (!application) {
|
||||
return res.status(404).json(responseFormat.error('保险申请不存在'));
|
||||
}
|
||||
|
||||
await application.destroy();
|
||||
|
||||
res.json(responseFormat.success(null, '保险申请删除成功'));
|
||||
} catch (error) {
|
||||
console.error('删除保险申请错误:', error);
|
||||
res.status(500).json(responseFormat.error('删除保险申请失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取保险申请统计
|
||||
const getApplicationStats = async (req, res) => {
|
||||
try {
|
||||
const stats = await InsuranceApplication.findAll({
|
||||
attributes: [
|
||||
'status',
|
||||
[InsuranceApplication.sequelize.fn('COUNT', InsuranceApplication.sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['status']
|
||||
});
|
||||
|
||||
const total = await InsuranceApplication.count();
|
||||
|
||||
res.json(responseFormat.success({
|
||||
stats,
|
||||
total
|
||||
}, '获取保险申请统计成功'));
|
||||
} catch (error) {
|
||||
console.error('获取保险申请统计错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取保险申请统计失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取参保类型选项
|
||||
const getInsuranceCategories = async (req, res) => {
|
||||
try {
|
||||
const categories = await InsuranceApplication.findAll({
|
||||
attributes: ['insurance_category'],
|
||||
where: {
|
||||
insurance_category: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
},
|
||||
group: ['insurance_category'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
const categoryList = categories.map(item => item.insurance_category).filter(Boolean);
|
||||
|
||||
// 添加一些常用的参保类型
|
||||
const defaultCategories = ['牛', '羊', '猪', '鸡', '鸭', '鹅'];
|
||||
const allCategories = [...new Set([...defaultCategories, ...categoryList])];
|
||||
|
||||
res.json(responseFormat.success(allCategories, '获取参保类型选项成功'));
|
||||
} catch (error) {
|
||||
console.error('获取参保类型选项错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取参保类型选项失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 导出保险申请数据
|
||||
const exportApplications = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 1000,
|
||||
applicantName,
|
||||
insuranceType,
|
||||
insuranceCategory,
|
||||
status
|
||||
} = req.query;
|
||||
|
||||
const where = {};
|
||||
|
||||
if (applicantName) {
|
||||
where.applicant_name = { [Op.like]: `%${applicantName}%` };
|
||||
}
|
||||
if (insuranceType) {
|
||||
where.insurance_type = insuranceType;
|
||||
}
|
||||
if (insuranceCategory) {
|
||||
where.insurance_category = insuranceCategory;
|
||||
}
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const applications = await InsuranceApplication.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insuranceTypeInfo',
|
||||
attributes: ['name', 'description']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'createdByUser',
|
||||
attributes: ['username', 'real_name']
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: (parseInt(page) - 1) * parseInt(limit)
|
||||
});
|
||||
|
||||
// 简单的CSV格式导出
|
||||
const csvHeader = '申请编号,申请人姓名,身份证号,联系电话,参保类型,保险类型,保险金额,保险期限,地址,状态,申请时间,备注\n';
|
||||
const csvData = applications.map(app => {
|
||||
const statusMap = {
|
||||
'pending': '待审核',
|
||||
'initial_approved': '初审通过',
|
||||
'under_review': '复审中',
|
||||
'approved': '已通过',
|
||||
'rejected': '已拒绝'
|
||||
};
|
||||
|
||||
return [
|
||||
app.application_number || '',
|
||||
app.applicant_name || '',
|
||||
app.id_card || '',
|
||||
app.phone || '',
|
||||
app.insurance_category || '',
|
||||
app.insurance_type || '',
|
||||
app.insurance_amount || '',
|
||||
app.insurance_period || '',
|
||||
app.address || '',
|
||||
statusMap[app.status] || app.status,
|
||||
app.created_at ? new Date(app.created_at).toLocaleString('zh-CN') : '',
|
||||
app.remarks || ''
|
||||
].map(field => `"${String(field).replace(/"/g, '""')}"`).join(',');
|
||||
}).join('\n');
|
||||
|
||||
const csvContent = csvHeader + csvData;
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="insurance_applications_${new Date().toISOString().slice(0, 10)}.csv"`);
|
||||
res.send('\uFEFF' + csvContent); // 添加BOM以支持中文
|
||||
} catch (error) {
|
||||
console.error('导出保险申请数据错误:', error);
|
||||
res.status(500).json(responseFormat.error('导出保险申请数据失败'));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getApplications,
|
||||
createApplication,
|
||||
getApplicationById,
|
||||
updateApplication,
|
||||
reviewApplication,
|
||||
deleteApplication,
|
||||
getApplicationStats,
|
||||
getInsuranceCategories,
|
||||
exportApplications
|
||||
};
|
||||
@@ -4,7 +4,21 @@ const responseFormat = require('../utils/response');
|
||||
|
||||
// 获取保险类型列表
|
||||
const getInsuranceTypes = async (req, res) => {
|
||||
const requestStartTime = new Date();
|
||||
|
||||
try {
|
||||
// ========== 后端接收数据日志 ==========
|
||||
console.log('🔵 [后端] 接收到获取险种列表请求');
|
||||
console.log('📥 [后端] 请求时间:', requestStartTime.toISOString());
|
||||
console.log('📥 [后端] 请求方法:', req.method);
|
||||
console.log('📥 [后端] 请求路径:', req.path);
|
||||
console.log('📥 [后端] 查询参数:', JSON.stringify(req.query, null, 2));
|
||||
console.log('📥 [后端] 用户信息:', req.user ? {
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
role: req.user.role
|
||||
} : '未认证用户');
|
||||
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
@@ -14,40 +28,122 @@ const getInsuranceTypes = async (req, res) => {
|
||||
service_area,
|
||||
on_sale_status
|
||||
} = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const whereClause = {};
|
||||
if (name) {
|
||||
whereClause.name = { [Op.like]: `%${name}%` };
|
||||
}
|
||||
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: [['add_time', 'DESC'], ['created_at', 'DESC']]
|
||||
// ========== 后端数据处理日志 ==========
|
||||
console.log('⚙️ [后端] 开始处理查询参数');
|
||||
console.log('⚙️ [后端] 原始查询参数:', {
|
||||
page, pageSize, name, status, applicable_livestock, service_area, on_sale_status
|
||||
});
|
||||
|
||||
res.json(responseFormat.pagination(rows, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(pageSize),
|
||||
const processedParams = {
|
||||
page: parseInt(page) || 1,
|
||||
pageSize: parseInt(pageSize) || 10,
|
||||
name: name ? String(name).trim() : null,
|
||||
status: status ? String(status).trim() : null,
|
||||
applicable_livestock: applicable_livestock ? String(applicable_livestock).trim() : null,
|
||||
service_area: service_area ? String(service_area).trim() : null,
|
||||
on_sale_status: on_sale_status !== undefined && on_sale_status !== '' ? on_sale_status === 'true' : null
|
||||
};
|
||||
|
||||
console.log('⚙️ [后端] 处理后的查询参数:', processedParams);
|
||||
|
||||
const offset = (processedParams.page - 1) * processedParams.pageSize;
|
||||
console.log('⚙️ [后端] 分页计算 - offset:', offset, 'limit:', processedParams.pageSize);
|
||||
|
||||
const whereClause = {};
|
||||
if (processedParams.name) {
|
||||
whereClause.name = processedParams.name; // 精确查询
|
||||
}
|
||||
if (processedParams.status) {
|
||||
whereClause.status = processedParams.status;
|
||||
}
|
||||
if (processedParams.applicable_livestock) {
|
||||
whereClause.applicable_livestock = { [Op.like]: `%${processedParams.applicable_livestock}%` };
|
||||
}
|
||||
if (processedParams.service_area) {
|
||||
whereClause.service_area = { [Op.like]: `%${processedParams.service_area}%` };
|
||||
}
|
||||
if (processedParams.on_sale_status !== null) {
|
||||
whereClause.on_sale_status = processedParams.on_sale_status;
|
||||
}
|
||||
|
||||
console.log('⚙️ [后端] 构建的WHERE条件:', whereClause);
|
||||
|
||||
const queryOptions = {
|
||||
where: whereClause,
|
||||
limit: processedParams.pageSize,
|
||||
offset: offset,
|
||||
order: [['add_time', 'DESC'], ['created_at', 'DESC']]
|
||||
};
|
||||
|
||||
console.log('⚙️ [后端] 数据库查询选项:', queryOptions);
|
||||
console.log('⚙️ [后端] 开始执行数据库查询...');
|
||||
|
||||
const { count, rows } = await InsuranceType.findAndCountAll(queryOptions);
|
||||
|
||||
console.log('✅ [后端] 数据库查询完成');
|
||||
console.log('✅ [后端] 查询结果统计:', {
|
||||
totalCount: count,
|
||||
returnedRows: rows.length,
|
||||
currentPage: processedParams.page,
|
||||
pageSize: processedParams.pageSize,
|
||||
totalPages: Math.ceil(count / processedParams.pageSize)
|
||||
});
|
||||
|
||||
// ========== 后端响应数据日志 ==========
|
||||
const responseEndTime = new Date();
|
||||
const processingTime = responseEndTime - requestStartTime;
|
||||
|
||||
const paginationData = {
|
||||
page: processedParams.page,
|
||||
limit: processedParams.pageSize,
|
||||
total: count
|
||||
}, '获取险种列表成功'));
|
||||
};
|
||||
|
||||
const successResponse = responseFormat.pagination(rows, paginationData, '获取险种列表成功');
|
||||
|
||||
console.log('📤 [后端] 准备发送成功响应');
|
||||
console.log('📤 [后端] 响应时间:', responseEndTime.toISOString());
|
||||
console.log('📤 [后端] 处理耗时:', processingTime + 'ms');
|
||||
console.log('📤 [后端] 响应状态码:', 200);
|
||||
console.log('📤 [后端] 响应数据结构:', {
|
||||
success: successResponse.success,
|
||||
message: successResponse.message,
|
||||
dataType: typeof successResponse.data,
|
||||
dataLength: Array.isArray(successResponse.data) ? successResponse.data.length : 'not array',
|
||||
paginationType: typeof successResponse.pagination,
|
||||
paginationKeys: successResponse.pagination ? Object.keys(successResponse.pagination) : 'no pagination'
|
||||
});
|
||||
console.log('📤 [后端] 分页信息:', successResponse.pagination);
|
||||
console.log('📤 [后端] 返回的险种数据概览:', rows.map(item => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
status: item.status,
|
||||
on_sale_status: item.on_sale_status
|
||||
})));
|
||||
|
||||
res.json(successResponse);
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取险种列表错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取险种列表失败'));
|
||||
// ========== 后端错误处理日志 ==========
|
||||
const errorEndTime = new Date();
|
||||
const processingTime = errorEndTime - requestStartTime;
|
||||
|
||||
console.error('❌ [后端] 获取险种列表发生错误');
|
||||
console.error('❌ [后端] 错误时间:', errorEndTime.toISOString());
|
||||
console.error('❌ [后端] 处理耗时:', processingTime + 'ms');
|
||||
console.error('❌ [后端] 错误类型:', error.name);
|
||||
console.error('❌ [后端] 错误消息:', error.message);
|
||||
console.error('❌ [后端] 错误堆栈:', error.stack);
|
||||
|
||||
const errorResponse = responseFormat.error('获取险种列表失败');
|
||||
|
||||
console.log('📤 [后端] 发送错误响应:', {
|
||||
statusCode: 500,
|
||||
response: errorResponse
|
||||
});
|
||||
|
||||
res.status(500).json(errorResponse);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,7 +166,26 @@ const getInsuranceTypeById = async (req, res) => {
|
||||
|
||||
// 创建险种
|
||||
const createInsuranceType = async (req, res) => {
|
||||
const requestStartTime = new Date();
|
||||
|
||||
try {
|
||||
// ========== 后端接收数据日志 ==========
|
||||
console.log('🔵 [后端] 接收到创建险种请求');
|
||||
console.log('📥 [后端] 请求时间:', requestStartTime.toISOString());
|
||||
console.log('📥 [后端] 请求方法:', req.method);
|
||||
console.log('📥 [后端] 请求路径:', req.path);
|
||||
console.log('📥 [后端] 请求头信息:', {
|
||||
'content-type': req.headers['content-type'],
|
||||
'authorization': req.headers.authorization ? '已提供' : '未提供',
|
||||
'user-agent': req.headers['user-agent']
|
||||
});
|
||||
console.log('📥 [后端] 原始请求体数据:', JSON.stringify(req.body, null, 2));
|
||||
console.log('📥 [后端] 用户信息:', req.user ? {
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
role: req.user.role
|
||||
} : '未认证用户');
|
||||
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
@@ -88,13 +203,9 @@ const createInsuranceType = async (req, res) => {
|
||||
status = 'active'
|
||||
} = req.body;
|
||||
|
||||
// 检查名称是否已存在
|
||||
const existingType = await InsuranceType.findOne({ where: { name } });
|
||||
if (existingType) {
|
||||
return res.status(400).json(responseFormat.error('险种名称已存在'));
|
||||
}
|
||||
|
||||
const insuranceType = await InsuranceType.create({
|
||||
// ========== 后端数据处理日志 ==========
|
||||
console.log('⚙️ [后端] 开始数据处理和验证');
|
||||
console.log('⚙️ [后端] 解构后的数据:', {
|
||||
name,
|
||||
description,
|
||||
applicable_livestock,
|
||||
@@ -104,22 +215,116 @@ const createInsuranceType = async (req, res) => {
|
||||
coverage_amount_max,
|
||||
premium_rate,
|
||||
service_area,
|
||||
add_time: add_time || new Date(),
|
||||
add_time,
|
||||
on_sale_status,
|
||||
sort_order,
|
||||
remarks,
|
||||
status,
|
||||
created_by: req.user?.id
|
||||
status
|
||||
});
|
||||
|
||||
res.status(201).json(responseFormat.success(insuranceType, '创建险种成功'));
|
||||
// 数据类型验证和转换
|
||||
const processedData = {
|
||||
name: name ? String(name).trim() : null,
|
||||
description: description ? String(description).trim() : null,
|
||||
applicable_livestock: Array.isArray(applicable_livestock) ?
|
||||
applicable_livestock.join(',') :
|
||||
(applicable_livestock ? String(applicable_livestock).trim() : null),
|
||||
insurance_term: insurance_term ? Number(insurance_term) : null,
|
||||
policy_form: policy_form ? String(policy_form).trim() : null,
|
||||
coverage_amount_min: coverage_amount_min ? Number(coverage_amount_min) : null,
|
||||
coverage_amount_max: coverage_amount_max ? Number(coverage_amount_max) : null,
|
||||
premium_rate: premium_rate ? Number(premium_rate) : null,
|
||||
service_area: service_area ? String(service_area).trim() : null,
|
||||
add_time: add_time ? new Date(add_time) : new Date(),
|
||||
on_sale_status: Boolean(on_sale_status),
|
||||
sort_order: sort_order ? Number(sort_order) : 0,
|
||||
remarks: remarks ? String(remarks).trim() : null,
|
||||
status: status || 'active'
|
||||
};
|
||||
|
||||
console.log('⚙️ [后端] 处理后的数据:', processedData);
|
||||
|
||||
// 检查名称是否已存在
|
||||
console.log('⚙️ [后端] 检查险种名称是否已存在:', processedData.name);
|
||||
const existingType = await InsuranceType.findOne({ where: { name: processedData.name } });
|
||||
|
||||
if (existingType) {
|
||||
console.log('❌ [后端] 险种名称已存在:', existingType.name);
|
||||
const errorResponse = responseFormat.error('险种名称已存在');
|
||||
console.log('📤 [后端] 发送错误响应:', errorResponse);
|
||||
return res.status(400).json(errorResponse);
|
||||
}
|
||||
|
||||
console.log('✅ [后端] 险种名称验证通过,开始创建数据');
|
||||
|
||||
// 准备数据库插入数据
|
||||
const dbInsertData = {
|
||||
...processedData,
|
||||
created_by: req.user?.id || null
|
||||
};
|
||||
|
||||
console.log('⚙️ [后端] 准备插入数据库的数据:', dbInsertData);
|
||||
|
||||
const insuranceType = await InsuranceType.create(dbInsertData);
|
||||
|
||||
console.log('✅ [后端] 险种创建成功,数据库返回:', {
|
||||
id: insuranceType.id,
|
||||
name: insuranceType.name,
|
||||
created_at: insuranceType.created_at
|
||||
});
|
||||
|
||||
// ========== 后端响应数据日志 ==========
|
||||
const responseEndTime = new Date();
|
||||
const processingTime = responseEndTime - requestStartTime;
|
||||
|
||||
const successResponse = responseFormat.success(insuranceType, '创建险种成功');
|
||||
|
||||
console.log('📤 [后端] 准备发送成功响应');
|
||||
console.log('📤 [后端] 响应时间:', responseEndTime.toISOString());
|
||||
console.log('📤 [后端] 处理耗时:', processingTime + 'ms');
|
||||
console.log('📤 [后端] 响应状态码:', 201);
|
||||
console.log('📤 [后端] 响应数据结构:', {
|
||||
success: successResponse.success,
|
||||
message: successResponse.message,
|
||||
dataType: typeof successResponse.data,
|
||||
dataId: successResponse.data?.id,
|
||||
dataKeys: Object.keys(successResponse.data || {})
|
||||
});
|
||||
console.log('📤 [后端] 完整响应数据:', JSON.stringify(successResponse, null, 2));
|
||||
|
||||
res.status(201).json(successResponse);
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建险种错误:', error);
|
||||
// ========== 后端错误处理日志 ==========
|
||||
const errorEndTime = new Date();
|
||||
const processingTime = errorEndTime - requestStartTime;
|
||||
|
||||
console.error('❌ [后端] 创建险种发生错误');
|
||||
console.error('❌ [后端] 错误时间:', errorEndTime.toISOString());
|
||||
console.error('❌ [后端] 处理耗时:', processingTime + 'ms');
|
||||
console.error('❌ [后端] 错误类型:', error.name);
|
||||
console.error('❌ [后端] 错误消息:', error.message);
|
||||
console.error('❌ [后端] 错误堆栈:', error.stack);
|
||||
|
||||
let errorResponse;
|
||||
let statusCode = 500;
|
||||
|
||||
if (error.name === 'SequelizeValidationError') {
|
||||
const messages = error.errors.map(err => err.message);
|
||||
return res.status(400).json(responseFormat.error(messages.join(', ')));
|
||||
errorResponse = responseFormat.error(messages.join(', '));
|
||||
statusCode = 400;
|
||||
console.error('❌ [后端] 数据验证错误:', messages);
|
||||
} else {
|
||||
errorResponse = responseFormat.error('创建险种失败');
|
||||
console.error('❌ [后端] 服务器内部错误');
|
||||
}
|
||||
res.status(500).json(responseFormat.error('创建险种失败'));
|
||||
|
||||
console.log('📤 [后端] 发送错误响应:', {
|
||||
statusCode,
|
||||
response: errorResponse
|
||||
});
|
||||
|
||||
res.status(statusCode).json(errorResponse);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,10 +367,18 @@ const updateInsuranceType = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理applicable_livestock数据类型转换
|
||||
let processedApplicableLivestock = insuranceType.applicable_livestock;
|
||||
if (applicable_livestock !== undefined) {
|
||||
processedApplicableLivestock = Array.isArray(applicable_livestock) ?
|
||||
applicable_livestock.join(',') :
|
||||
(applicable_livestock ? String(applicable_livestock).trim() : null);
|
||||
}
|
||||
|
||||
await insuranceType.update({
|
||||
name: name || insuranceType.name,
|
||||
description: description !== undefined ? description : insuranceType.description,
|
||||
applicable_livestock: applicable_livestock !== undefined ? applicable_livestock : insuranceType.applicable_livestock,
|
||||
applicable_livestock: processedApplicableLivestock,
|
||||
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,
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
const LivestockType = require('../models/LivestockType');
|
||||
const User = require('../models/User');
|
||||
const responseFormat = require('../utils/response');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 获取牲畜类型列表
|
||||
const getLivestockTypes = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
is_active,
|
||||
page = 1,
|
||||
limit = 10
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
|
||||
// 名称筛选
|
||||
if (name) {
|
||||
whereClause.name = { [Op.like]: `%${name}%` };
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (is_active !== undefined) {
|
||||
whereClause.is_active = is_active === 'true';
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { count, rows } = await LivestockType.findAndCountAll({
|
||||
where: whereClause,
|
||||
order: [['created_at', 'DESC']],
|
||||
offset,
|
||||
limit: parseInt(limit)
|
||||
});
|
||||
|
||||
res.json(responseFormat.pagination(rows, {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: count
|
||||
}, '获取牲畜类型列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取牲畜类型列表错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取牲畜类型列表失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取所有启用的牲畜类型(用于下拉选择)
|
||||
const getActiveLivestockTypes = async (req, res) => {
|
||||
try {
|
||||
const types = await LivestockType.findAll({
|
||||
where: { is_active: true },
|
||||
attributes: ['id', 'name', 'description', 'base_value', 'premium_rate'],
|
||||
order: [['name', 'ASC']]
|
||||
});
|
||||
|
||||
res.json(responseFormat.success(types, '获取启用牲畜类型成功'));
|
||||
} catch (error) {
|
||||
console.error('获取启用牲畜类型错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取启用牲畜类型失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 创建牲畜类型
|
||||
const createLivestockType = async (req, res) => {
|
||||
try {
|
||||
const typeData = req.body;
|
||||
|
||||
// 检查名称是否已存在
|
||||
const existingType = await LivestockType.findOne({
|
||||
where: { name: typeData.name }
|
||||
});
|
||||
|
||||
if (existingType) {
|
||||
return res.status(400).json(responseFormat.error('牲畜类型名称已存在'));
|
||||
}
|
||||
|
||||
const type = await LivestockType.create({
|
||||
...typeData,
|
||||
created_by: req.user?.id
|
||||
});
|
||||
|
||||
res.status(201).json(responseFormat.created(type, '牲畜类型创建成功'));
|
||||
} catch (error) {
|
||||
console.error('创建牲畜类型错误:', error);
|
||||
if (error.name === 'SequelizeValidationError') {
|
||||
const messages = error.errors.map(err => err.message);
|
||||
return res.status(400).json(responseFormat.error(messages.join(', ')));
|
||||
}
|
||||
res.status(500).json(responseFormat.error('创建牲畜类型失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取单个牲畜类型详情
|
||||
const getLivestockTypeById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const type = await LivestockType.findByPk(id);
|
||||
|
||||
if (!type) {
|
||||
return res.status(404).json(responseFormat.error('牲畜类型不存在'));
|
||||
}
|
||||
|
||||
res.json(responseFormat.success(type, '获取牲畜类型详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取牲畜类型详情错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取牲畜类型详情失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新牲畜类型
|
||||
const updateLivestockType = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const type = await LivestockType.findByPk(id);
|
||||
if (!type) {
|
||||
return res.status(404).json(responseFormat.error('牲畜类型不存在'));
|
||||
}
|
||||
|
||||
// 如果更新名称,检查是否与其他记录重复
|
||||
if (updateData.name && updateData.name !== type.name) {
|
||||
const existingType = await LivestockType.findOne({
|
||||
where: {
|
||||
name: updateData.name,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingType) {
|
||||
return res.status(400).json(responseFormat.error('牲畜类型名称已存在'));
|
||||
}
|
||||
}
|
||||
|
||||
updateData.updated_by = req.user?.id;
|
||||
|
||||
await type.update(updateData);
|
||||
|
||||
res.json(responseFormat.success(type, '牲畜类型更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新牲畜类型错误:', error);
|
||||
if (error.name === 'SequelizeValidationError') {
|
||||
const messages = error.errors.map(err => err.message);
|
||||
return res.status(400).json(responseFormat.error(messages.join(', ')));
|
||||
}
|
||||
res.status(500).json(responseFormat.error('更新牲畜类型失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除牲畜类型(软删除 - 设置为不启用)
|
||||
const deleteLivestockType = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const type = await LivestockType.findByPk(id);
|
||||
if (!type) {
|
||||
return res.status(404).json(responseFormat.error('牲畜类型不存在'));
|
||||
}
|
||||
|
||||
// 检查是否有关联的保单
|
||||
const LivestockPolicy = require('../models/LivestockPolicy');
|
||||
const relatedPolicies = await LivestockPolicy.count({
|
||||
where: { livestock_type_id: id }
|
||||
});
|
||||
|
||||
if (relatedPolicies > 0) {
|
||||
// 如果有关联保单,只能设置为不启用
|
||||
await type.update({
|
||||
is_active: false,
|
||||
updated_by: req.user?.id
|
||||
});
|
||||
return res.json(responseFormat.success(null, '牲畜类型已设置为不启用(存在关联保单)'));
|
||||
}
|
||||
|
||||
// 如果没有关联保单,可以物理删除
|
||||
await type.destroy();
|
||||
|
||||
res.json(responseFormat.success(null, '牲畜类型删除成功'));
|
||||
} catch (error) {
|
||||
console.error('删除牲畜类型错误:', error);
|
||||
res.status(500).json(responseFormat.error('删除牲畜类型失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 批量更新牲畜类型状态
|
||||
const batchUpdateLivestockTypeStatus = async (req, res) => {
|
||||
try {
|
||||
const { ids, is_active } = req.body;
|
||||
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json(responseFormat.error('请提供有效的ID列表'));
|
||||
}
|
||||
|
||||
await LivestockType.update(
|
||||
{
|
||||
is_active,
|
||||
updated_by: req.user?.id
|
||||
},
|
||||
{
|
||||
where: { id: { [Op.in]: ids } }
|
||||
}
|
||||
);
|
||||
|
||||
res.json(responseFormat.success(null, '批量更新牲畜类型状态成功'));
|
||||
} catch (error) {
|
||||
console.error('批量更新牲畜类型状态错误:', error);
|
||||
res.status(500).json(responseFormat.error('批量更新牲畜类型状态失败'));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getLivestockTypes,
|
||||
getActiveLivestockTypes,
|
||||
createLivestockType,
|
||||
getLivestockTypeById,
|
||||
updateLivestockType,
|
||||
deleteLivestockType,
|
||||
batchUpdateLivestockTypeStatus
|
||||
};
|
||||
@@ -1,178 +0,0 @@
|
||||
const { User, Role, Menu } = require('../models');
|
||||
|
||||
/**
|
||||
* 获取菜单列表
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
exports.getMenus = async (req, res) => {
|
||||
try {
|
||||
console.log('开始获取菜单,用户信息:', req.user);
|
||||
|
||||
// 获取用户ID(从JWT中解析或通过其他方式获取)
|
||||
const userId = req.user?.id; // 假设通过认证中间件解析后存在
|
||||
|
||||
console.log('用户ID:', userId);
|
||||
|
||||
// 如果没有用户ID,返回基础菜单
|
||||
if (!userId) {
|
||||
console.log('没有用户ID,返回基础菜单');
|
||||
const menus = await Menu.findAll({
|
||||
where: {
|
||||
parent_id: null,
|
||||
show: true,
|
||||
status: 'active'
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Menu,
|
||||
as: 'children',
|
||||
where: {
|
||||
show: true,
|
||||
status: 'active'
|
||||
},
|
||||
required: false,
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
});
|
||||
|
||||
console.log('基础菜单查询结果:', menus.length);
|
||||
|
||||
return res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
data: menus,
|
||||
message: '获取菜单成功'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('查询用户信息...');
|
||||
// 获取用户信息和角色
|
||||
const user = await User.findByPk(userId, {
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
as: 'role',
|
||||
attributes: ['id', 'name', 'permissions']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
console.log('用户查询结果:', user ? '找到用户' : '用户不存在');
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
code: 404,
|
||||
status: 'error',
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取角色的权限列表
|
||||
const userPermissions = user.role?.permissions || [];
|
||||
console.log('用户权限:', userPermissions);
|
||||
|
||||
console.log('查询菜单数据...');
|
||||
// 查询菜单,这里简化处理,实际应用中可能需要根据权限过滤
|
||||
const menus = await Menu.findAll({
|
||||
where: {
|
||||
parent_id: null,
|
||||
show: true,
|
||||
status: 'active'
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Menu,
|
||||
as: 'children',
|
||||
where: {
|
||||
show: true,
|
||||
status: 'active'
|
||||
},
|
||||
required: false,
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
});
|
||||
|
||||
console.log('菜单查询结果:', menus.length);
|
||||
|
||||
// 这里可以添加根据权限过滤菜单的逻辑
|
||||
// 简化示例,假设所有用户都能看到所有激活的菜单
|
||||
|
||||
return res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
data: menus,
|
||||
message: '获取菜单成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取菜单失败:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '服务器内部错误',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有菜单(包括非激活状态,仅管理员可用)
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
*/
|
||||
exports.getAllMenus = async (req, res) => {
|
||||
try {
|
||||
// 检查用户是否为管理员(简化示例)
|
||||
const user = await User.findByPk(req.user?.id, {
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
attributes: ['id', 'name', 'permissions']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!user || !user.Role || !user.Role.permissions.includes('*:*')) {
|
||||
return res.status(403).json({
|
||||
code: 403,
|
||||
status: 'error',
|
||||
message: '没有权限查看所有菜单'
|
||||
});
|
||||
}
|
||||
|
||||
// 查询所有菜单
|
||||
const menus = await Menu.findAll({
|
||||
where: {
|
||||
parent_id: null
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Menu,
|
||||
as: 'children',
|
||||
required: false,
|
||||
order: [['order', 'ASC']]
|
||||
}
|
||||
],
|
||||
order: [['order', 'ASC']]
|
||||
});
|
||||
|
||||
return res.json({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
data: menus,
|
||||
message: '获取所有菜单成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取所有菜单失败:', error);
|
||||
return res.status(500).json({
|
||||
code: 500,
|
||||
status: 'error',
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,344 +0,0 @@
|
||||
const { OperationLog, User } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
const ExcelJS = require('exceljs');
|
||||
|
||||
/**
|
||||
* 操作日志控制器
|
||||
*/
|
||||
class OperationLogController {
|
||||
/**
|
||||
* 获取操作日志列表
|
||||
*/
|
||||
async getOperationLogs(req, res) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
user_id,
|
||||
operation_type,
|
||||
operation_module,
|
||||
status,
|
||||
start_date,
|
||||
end_date,
|
||||
keyword
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
if (user_id) {
|
||||
whereConditions.user_id = user_id;
|
||||
}
|
||||
|
||||
if (operation_type) {
|
||||
whereConditions.operation_type = operation_type;
|
||||
}
|
||||
|
||||
if (operation_module) {
|
||||
whereConditions.operation_module = operation_module;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.status = status;
|
||||
}
|
||||
|
||||
// 时间范围查询
|
||||
if (start_date || end_date) {
|
||||
whereConditions.created_at = {};
|
||||
if (start_date) {
|
||||
whereConditions.created_at[Op.gte] = new Date(start_date);
|
||||
}
|
||||
if (end_date) {
|
||||
const endDateTime = new Date(end_date);
|
||||
endDateTime.setHours(23, 59, 59, 999);
|
||||
whereConditions.created_at[Op.lte] = endDateTime;
|
||||
}
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
whereConditions[Op.or] = [
|
||||
{ operation_content: { [Op.like]: `%${keyword}%` } },
|
||||
{ operation_target: { [Op.like]: `%${keyword}%` } },
|
||||
{ request_url: { [Op.like]: `%${keyword}%` } },
|
||||
{ ip_address: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
// 分页参数
|
||||
const offset = (parseInt(page) - 1) * parseInt(limit);
|
||||
|
||||
// 查询操作日志
|
||||
const { count, rows } = await OperationLog.findAndCountAll({
|
||||
where: whereConditions,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name', 'email']
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: offset
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(count / parseInt(limit));
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
message: '获取操作日志列表成功',
|
||||
data: {
|
||||
logs: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
totalPages: totalPages
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志列表失败:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message: '获取操作日志列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作日志统计信息
|
||||
*/
|
||||
async getOperationStats(req, res) {
|
||||
try {
|
||||
const {
|
||||
user_id,
|
||||
start_date,
|
||||
end_date
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
if (user_id) {
|
||||
whereConditions.user_id = user_id;
|
||||
}
|
||||
|
||||
// 时间范围查询
|
||||
if (start_date || end_date) {
|
||||
whereConditions.created_at = {};
|
||||
if (start_date) {
|
||||
whereConditions.created_at[Op.gte] = new Date(start_date);
|
||||
}
|
||||
if (end_date) {
|
||||
const endDateTime = new Date(end_date);
|
||||
endDateTime.setHours(23, 59, 59, 999);
|
||||
whereConditions.created_at[Op.lte] = endDateTime;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const stats = await OperationLog.getOperationStats(whereConditions);
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
message: '获取操作日志统计成功',
|
||||
data: stats
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志统计失败:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message: '获取操作日志统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作日志详情
|
||||
*/
|
||||
async getOperationLogById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const log = await OperationLog.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!log) {
|
||||
return res.status(404).json({
|
||||
status: 'error',
|
||||
message: '操作日志不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: 'success',
|
||||
message: '获取操作日志详情成功',
|
||||
data: log
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志详情失败:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message: '获取操作日志详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出操作日志
|
||||
*/
|
||||
async exportOperationLogs(req, res) {
|
||||
try {
|
||||
const {
|
||||
user_id,
|
||||
operation_type,
|
||||
operation_module,
|
||||
status,
|
||||
start_date,
|
||||
end_date,
|
||||
keyword
|
||||
} = req.body;
|
||||
|
||||
// 构建查询条件
|
||||
const whereConditions = {};
|
||||
|
||||
if (user_id) {
|
||||
whereConditions.user_id = user_id;
|
||||
}
|
||||
|
||||
if (operation_type) {
|
||||
whereConditions.operation_type = operation_type;
|
||||
}
|
||||
|
||||
if (operation_module) {
|
||||
whereConditions.operation_module = operation_module;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.status = status;
|
||||
}
|
||||
|
||||
// 时间范围查询
|
||||
if (start_date || end_date) {
|
||||
whereConditions.created_at = {};
|
||||
if (start_date) {
|
||||
whereConditions.created_at[Op.gte] = new Date(start_date);
|
||||
}
|
||||
if (end_date) {
|
||||
const endDateTime = new Date(end_date);
|
||||
endDateTime.setHours(23, 59, 59, 999);
|
||||
whereConditions.created_at[Op.lte] = endDateTime;
|
||||
}
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
whereConditions[Op.or] = [
|
||||
{ operation_content: { [Op.like]: `%${keyword}%` } },
|
||||
{ operation_target: { [Op.like]: `%${keyword}%` } },
|
||||
{ request_url: { [Op.like]: `%${keyword}%` } },
|
||||
{ ip_address: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
// 查询所有符合条件的日志
|
||||
const logs = await OperationLog.findAll({
|
||||
where: whereConditions,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name', 'email']
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
// 创建Excel工作簿
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('操作日志');
|
||||
|
||||
// 设置表头
|
||||
worksheet.columns = [
|
||||
{ header: 'ID', key: 'id', width: 10 },
|
||||
{ header: '操作用户', key: 'username', width: 15 },
|
||||
{ header: '真实姓名', key: 'real_name', width: 15 },
|
||||
{ header: '操作类型', key: 'operation_type', width: 15 },
|
||||
{ header: '操作模块', key: 'operation_module', width: 15 },
|
||||
{ header: '操作内容', key: 'operation_content', width: 30 },
|
||||
{ header: '操作目标', key: 'operation_target', width: 20 },
|
||||
{ header: '请求方法', key: 'request_method', width: 10 },
|
||||
{ header: '请求URL', key: 'request_url', width: 30 },
|
||||
{ header: 'IP地址', key: 'ip_address', width: 15 },
|
||||
{ header: '执行时间(ms)', key: 'execution_time', width: 12 },
|
||||
{ header: '状态', key: 'status', width: 10 },
|
||||
{ header: '错误信息', key: 'error_message', width: 30 },
|
||||
{ header: '创建时间', key: 'created_at', width: 20 }
|
||||
];
|
||||
|
||||
// 添加数据
|
||||
logs.forEach(log => {
|
||||
worksheet.addRow({
|
||||
id: log.id,
|
||||
username: log.user ? log.user.username : '',
|
||||
real_name: log.user ? log.user.real_name : '',
|
||||
operation_type: log.operation_type,
|
||||
operation_module: log.operation_module,
|
||||
operation_content: log.operation_content,
|
||||
operation_target: log.operation_target,
|
||||
request_method: log.request_method,
|
||||
request_url: log.request_url,
|
||||
ip_address: log.ip_address,
|
||||
execution_time: log.execution_time,
|
||||
status: log.status,
|
||||
error_message: log.error_message,
|
||||
created_at: log.created_at
|
||||
});
|
||||
});
|
||||
|
||||
// 设置响应头
|
||||
const filename = `操作日志_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
|
||||
|
||||
// 写入响应
|
||||
await workbook.xlsx.write(res);
|
||||
res.end();
|
||||
|
||||
// 记录导出操作日志
|
||||
await OperationLog.logOperation({
|
||||
user_id: req.user.id,
|
||||
operation_type: 'export',
|
||||
operation_module: 'operation_logs',
|
||||
operation_content: '导出操作日志',
|
||||
operation_target: `导出${logs.length}条记录`,
|
||||
request_method: 'POST',
|
||||
request_url: '/api/operation-logs/export',
|
||||
request_params: req.body,
|
||||
ip_address: req.ip,
|
||||
user_agent: req.get('User-Agent'),
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('导出操作日志失败:', error);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message: '导出操作日志失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new OperationLogController();
|
||||
@@ -1,415 +0,0 @@
|
||||
const { Permission, Role, Menu, RolePermission, MenuPermission } = require('../models');
|
||||
const responseFormat = require('../utils/response');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 构建权限树形结构
|
||||
*/
|
||||
function buildPermissionTree(permissions, parentId = null) {
|
||||
const tree = [];
|
||||
|
||||
for (const permission of permissions) {
|
||||
if (permission.parent_id === parentId) {
|
||||
const children = buildPermissionTree(permissions, permission.id);
|
||||
const node = {
|
||||
...permission.toJSON(),
|
||||
children: children.length > 0 ? children : undefined
|
||||
};
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限管理控制器
|
||||
*/
|
||||
class PermissionController {
|
||||
/**
|
||||
* 获取权限列表
|
||||
*/
|
||||
async getPermissions(req, res) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
module,
|
||||
type,
|
||||
status = 'active',
|
||||
keyword
|
||||
} = req.query;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
const where = { status };
|
||||
|
||||
// 模块筛选
|
||||
if (module) {
|
||||
where.module = module;
|
||||
}
|
||||
|
||||
// 类型筛选
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.like]: `%${keyword}%` } },
|
||||
{ code: { [Op.like]: `%${keyword}%` } },
|
||||
{ description: { [Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await Permission.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
as: 'parent',
|
||||
attributes: ['id', 'name', 'code']
|
||||
},
|
||||
{
|
||||
model: Permission,
|
||||
as: 'children',
|
||||
attributes: ['id', 'name', 'code', 'type']
|
||||
}
|
||||
],
|
||||
order: [['sort_order', 'ASC'], ['id', 'ASC']],
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset)
|
||||
});
|
||||
|
||||
res.json(responseFormat.success({
|
||||
permissions: rows,
|
||||
pagination: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
pages: Math.ceil(count / limit)
|
||||
}
|
||||
}, '获取权限列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取权限列表失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取权限列表失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限树形结构
|
||||
*/
|
||||
async getPermissionTree(req, res) {
|
||||
try {
|
||||
const { module, type } = req.query;
|
||||
const where = { status: 'active' };
|
||||
|
||||
if (module) {
|
||||
where.module = module;
|
||||
}
|
||||
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
const permissions = await Permission.findAll({
|
||||
where,
|
||||
order: [['sort_order', 'ASC'], ['id', 'ASC']]
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
const tree = buildPermissionTree(permissions);
|
||||
|
||||
res.json(responseFormat.success(tree, '获取权限树成功'));
|
||||
} catch (error) {
|
||||
console.error('获取权限树失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取权限树失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个权限详情
|
||||
*/
|
||||
async getPermissionById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const permission = await Permission.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
as: 'parent',
|
||||
attributes: ['id', 'name', 'code']
|
||||
},
|
||||
{
|
||||
model: Permission,
|
||||
as: 'children',
|
||||
attributes: ['id', 'name', 'code', 'type']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!permission) {
|
||||
return res.status(404).json(responseFormat.error('权限不存在'));
|
||||
}
|
||||
|
||||
res.json(responseFormat.success(permission, '获取权限详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取权限详情失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取权限详情失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建权限
|
||||
*/
|
||||
async createPermission(req, res) {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
code,
|
||||
description,
|
||||
module,
|
||||
type = 'operation',
|
||||
parent_id,
|
||||
sort_order = 0
|
||||
} = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!name || !code || !module) {
|
||||
return res.status(400).json(responseFormat.error('权限名称、权限代码和所属模块为必填项'));
|
||||
}
|
||||
|
||||
// 检查权限代码是否已存在
|
||||
const existingPermission = await Permission.findOne({ where: { code } });
|
||||
if (existingPermission) {
|
||||
return res.status(400).json(responseFormat.error('权限代码已存在'));
|
||||
}
|
||||
|
||||
// 如果有父权限,验证父权限是否存在
|
||||
if (parent_id) {
|
||||
const parentPermission = await Permission.findByPk(parent_id);
|
||||
if (!parentPermission) {
|
||||
return res.status(400).json(responseFormat.error('父权限不存在'));
|
||||
}
|
||||
}
|
||||
|
||||
const permission = await Permission.create({
|
||||
name,
|
||||
code,
|
||||
description,
|
||||
module,
|
||||
type,
|
||||
parent_id,
|
||||
sort_order,
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
res.status(201).json(responseFormat.created(permission, '创建权限成功'));
|
||||
} catch (error) {
|
||||
console.error('创建权限失败:', error);
|
||||
res.status(500).json(responseFormat.error('创建权限失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新权限
|
||||
*/
|
||||
async updatePermission(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
name,
|
||||
code,
|
||||
description,
|
||||
module,
|
||||
type,
|
||||
parent_id,
|
||||
sort_order,
|
||||
status
|
||||
} = req.body;
|
||||
|
||||
const permission = await Permission.findByPk(id);
|
||||
if (!permission) {
|
||||
return res.status(404).json(responseFormat.error('权限不存在'));
|
||||
}
|
||||
|
||||
// 如果修改了权限代码,检查是否与其他权限冲突
|
||||
if (code && code !== permission.code) {
|
||||
const existingPermission = await Permission.findOne({
|
||||
where: {
|
||||
code,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
if (existingPermission) {
|
||||
return res.status(400).json(responseFormat.error('权限代码已存在'));
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有父权限,验证父权限是否存在且不是自己
|
||||
if (parent_id) {
|
||||
if (parent_id == id) {
|
||||
return res.status(400).json(responseFormat.error('不能将自己设为父权限'));
|
||||
}
|
||||
const parentPermission = await Permission.findByPk(parent_id);
|
||||
if (!parentPermission) {
|
||||
return res.status(400).json(responseFormat.error('父权限不存在'));
|
||||
}
|
||||
}
|
||||
|
||||
await permission.update({
|
||||
name: name || permission.name,
|
||||
code: code || permission.code,
|
||||
description: description !== undefined ? description : permission.description,
|
||||
module: module || permission.module,
|
||||
type: type || permission.type,
|
||||
parent_id: parent_id !== undefined ? parent_id : permission.parent_id,
|
||||
sort_order: sort_order !== undefined ? sort_order : permission.sort_order,
|
||||
status: status || permission.status
|
||||
});
|
||||
|
||||
res.json(responseFormat.success(permission, '更新权限成功'));
|
||||
} catch (error) {
|
||||
console.error('更新权限失败:', error);
|
||||
res.status(500).json(responseFormat.error('更新权限失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除权限
|
||||
*/
|
||||
async deletePermission(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const permission = await Permission.findByPk(id);
|
||||
if (!permission) {
|
||||
return res.status(404).json(responseFormat.error('权限不存在'));
|
||||
}
|
||||
|
||||
// 检查是否有子权限
|
||||
const childrenCount = await Permission.count({ where: { parent_id: id } });
|
||||
if (childrenCount > 0) {
|
||||
return res.status(400).json(responseFormat.error('该权限下还有子权限,无法删除'));
|
||||
}
|
||||
|
||||
// 检查是否有角色在使用该权限
|
||||
const rolePermissionCount = await RolePermission.count({ where: { permission_id: id } });
|
||||
if (rolePermissionCount > 0) {
|
||||
return res.status(400).json(responseFormat.error('该权限正在被角色使用,无法删除'));
|
||||
}
|
||||
|
||||
// 检查是否有菜单在使用该权限
|
||||
const menuPermissionCount = await MenuPermission.count({ where: { permission_id: id } });
|
||||
if (menuPermissionCount > 0) {
|
||||
return res.status(400).json(responseFormat.error('该权限正在被菜单使用,无法删除'));
|
||||
}
|
||||
|
||||
await permission.destroy();
|
||||
|
||||
res.json(responseFormat.success(null, '删除权限成功'));
|
||||
} catch (error) {
|
||||
console.error('删除权限失败:', error);
|
||||
res.status(500).json(responseFormat.error('删除权限失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色权限
|
||||
*/
|
||||
async getRolePermissions(req, res) {
|
||||
try {
|
||||
const { roleId } = req.params;
|
||||
|
||||
const role = await Role.findByPk(roleId, {
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
as: 'rolePermissions',
|
||||
through: {
|
||||
attributes: ['granted']
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
return res.status(404).json(responseFormat.error('角色不存在'));
|
||||
}
|
||||
|
||||
res.json(responseFormat.success({
|
||||
role: {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description
|
||||
},
|
||||
permissions: role.rolePermissions
|
||||
}, '获取角色权限成功'));
|
||||
} catch (error) {
|
||||
console.error('获取角色权限失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取角色权限失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配角色权限
|
||||
*/
|
||||
async assignRolePermissions(req, res) {
|
||||
try {
|
||||
const { roleId } = req.params;
|
||||
const { permissionIds } = req.body;
|
||||
|
||||
if (!Array.isArray(permissionIds)) {
|
||||
return res.status(400).json(responseFormat.error('权限ID列表格式错误'));
|
||||
}
|
||||
|
||||
const role = await Role.findByPk(roleId);
|
||||
if (!role) {
|
||||
return res.status(404).json(responseFormat.error('角色不存在'));
|
||||
}
|
||||
|
||||
// 删除现有的角色权限关联
|
||||
await RolePermission.destroy({ where: { role_id: roleId } });
|
||||
|
||||
// 创建新的角色权限关联
|
||||
if (permissionIds.length > 0) {
|
||||
const rolePermissions = permissionIds.map(permissionId => ({
|
||||
role_id: roleId,
|
||||
permission_id: permissionId,
|
||||
granted: true
|
||||
}));
|
||||
|
||||
await RolePermission.bulkCreate(rolePermissions);
|
||||
}
|
||||
|
||||
res.json(responseFormat.success(null, '分配角色权限成功'));
|
||||
} catch (error) {
|
||||
console.error('分配角色权限失败:', error);
|
||||
res.status(500).json(responseFormat.error('分配角色权限失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模块列表
|
||||
*/
|
||||
async getModules(req, res) {
|
||||
try {
|
||||
const modules = await Permission.findAll({
|
||||
attributes: ['module'],
|
||||
group: ['module'],
|
||||
order: [['module', 'ASC']]
|
||||
});
|
||||
|
||||
const moduleList = modules.map(item => item.module);
|
||||
|
||||
res.json(responseFormat.success(moduleList, '获取模块列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取模块列表失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取模块列表失败'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = new PermissionController();
|
||||
@@ -1,239 +0,0 @@
|
||||
const { Policy, InsuranceApplication, InsuranceType, User } = require('../models');
|
||||
const responseFormat = require('../utils/response');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 获取保单列表
|
||||
const getPolicies = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
policy_number, // 前端发送的参数名
|
||||
policyholder_name, // 前端发送的参数名
|
||||
insurance_type_id, // 前端发送的参数名
|
||||
status, // 前端发送的参数名
|
||||
page = 1,
|
||||
pageSize = 10 // 前端发送的参数名
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
|
||||
// 保单编号筛选
|
||||
if (policy_number) {
|
||||
whereClause.policy_no = { [Op.like]: `%${policy_number}%` };
|
||||
}
|
||||
|
||||
// 保单状态筛选
|
||||
if (status) {
|
||||
whereClause.policy_status = status;
|
||||
}
|
||||
|
||||
// 保险类型筛选
|
||||
if (insurance_type_id) {
|
||||
whereClause.insurance_type_id = insurance_type_id;
|
||||
}
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const { count, rows } = await Policy.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{
|
||||
model: InsuranceApplication,
|
||||
as: 'application',
|
||||
attributes: ['id', 'customer_name', 'customer_phone'],
|
||||
include: [
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insurance_type',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'customer',
|
||||
attributes: ['id', 'real_name', 'username']
|
||||
},
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insurance_type',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
],
|
||||
order: [['created_at', 'DESC']],
|
||||
offset,
|
||||
limit: parseInt(pageSize)
|
||||
});
|
||||
|
||||
// 处理返回数据,确保前端能够正确解析
|
||||
const processedRows = rows.map(row => {
|
||||
const policy = row.toJSON();
|
||||
return {
|
||||
id: policy.id,
|
||||
policy_number: policy.policy_no, // 映射字段名
|
||||
policyholder_name: policy.application?.customer_name || policy.customer?.real_name || '',
|
||||
insured_name: policy.application?.customer_name || policy.customer?.real_name || '',
|
||||
insurance_type_id: policy.insurance_type_id,
|
||||
insurance_type_name: policy.insurance_type?.name || '',
|
||||
premium_amount: parseFloat(policy.premium_amount) || 0,
|
||||
coverage_amount: parseFloat(policy.coverage_amount) || 0,
|
||||
start_date: policy.start_date,
|
||||
end_date: policy.end_date,
|
||||
status: policy.policy_status, // 映射字段名
|
||||
phone: policy.application?.customer_phone || '',
|
||||
email: policy.customer?.email || '',
|
||||
address: policy.application?.address || '',
|
||||
remarks: policy.terms_and_conditions || '',
|
||||
created_at: policy.created_at,
|
||||
updated_at: policy.updated_at
|
||||
};
|
||||
});
|
||||
|
||||
res.json(responseFormat.pagination(processedRows, {
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
total: count
|
||||
}, '获取保单列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取保单列表错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取保单列表失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 创建保单
|
||||
const createPolicy = async (req, res) => {
|
||||
try {
|
||||
const policyData = req.body;
|
||||
|
||||
// 生成保单编号
|
||||
const policyNo = `POL${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
||||
|
||||
const policy = await Policy.create({
|
||||
...policyData,
|
||||
policy_no: policyNo
|
||||
});
|
||||
|
||||
res.status(201).json(responseFormat.created(policy, '保单创建成功'));
|
||||
} catch (error) {
|
||||
console.error('创建保单错误:', error);
|
||||
res.status(500).json(responseFormat.error('创建保单失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取单个保单详情
|
||||
const getPolicyById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const policy = await Policy.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: InsuranceApplication,
|
||||
as: 'application',
|
||||
include: [
|
||||
{
|
||||
model: InsuranceType,
|
||||
as: 'insurance_type',
|
||||
attributes: ['id', 'name', 'description', 'premium_rate']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'customer',
|
||||
attributes: ['id', 'real_name', 'username', 'phone', 'email']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!policy) {
|
||||
return res.status(404).json(responseFormat.error('保单不存在'));
|
||||
}
|
||||
|
||||
res.json(responseFormat.success(policy, '获取保单详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取保单详情错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取保单详情失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新保单
|
||||
const updatePolicy = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const policy = await Policy.findByPk(id);
|
||||
if (!policy) {
|
||||
return res.status(404).json(responseFormat.error('保单不存在'));
|
||||
}
|
||||
|
||||
await policy.update(updateData);
|
||||
|
||||
res.json(responseFormat.success(policy, '保单更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新保单错误:', error);
|
||||
res.status(500).json(responseFormat.error('更新保单失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新保单状态
|
||||
const updatePolicyStatus = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { policy_status } = req.body;
|
||||
|
||||
const policy = await Policy.findByPk(id);
|
||||
if (!policy) {
|
||||
return res.status(404).json(responseFormat.error('保单不存在'));
|
||||
}
|
||||
|
||||
if (!['active', 'expired', 'cancelled', 'suspended'].includes(policy_status)) {
|
||||
return res.status(400).json(responseFormat.error('无效的保单状态'));
|
||||
}
|
||||
|
||||
await policy.update({ policy_status });
|
||||
|
||||
res.json(responseFormat.success(policy, '保单状态更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新保单状态错误:', error);
|
||||
res.status(500).json(responseFormat.error('更新保单状态失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取保单统计
|
||||
const getPolicyStats = async (req, res) => {
|
||||
try {
|
||||
const stats = await Policy.findAll({
|
||||
attributes: [
|
||||
'policy_status',
|
||||
[Policy.sequelize.fn('COUNT', Policy.sequelize.col('id')), 'count'],
|
||||
[Policy.sequelize.fn('SUM', Policy.sequelize.col('coverage_amount')), 'total_coverage'],
|
||||
[Policy.sequelize.fn('SUM', Policy.sequelize.col('premium_amount')), 'total_premium']
|
||||
],
|
||||
group: ['policy_status']
|
||||
});
|
||||
|
||||
const total = await Policy.count();
|
||||
const totalCoverage = await Policy.sum('coverage_amount');
|
||||
const totalPremium = await Policy.sum('premium_amount');
|
||||
|
||||
res.json(responseFormat.success({
|
||||
stats,
|
||||
total,
|
||||
total_coverage: totalCoverage || 0,
|
||||
total_premium: totalPremium || 0
|
||||
}, '获取保单统计成功'));
|
||||
} catch (error) {
|
||||
console.error('获取保单统计错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取保单统计失败'));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getPolicies,
|
||||
createPolicy,
|
||||
getPolicyById,
|
||||
updatePolicy,
|
||||
updatePolicyStatus,
|
||||
getPolicyStats
|
||||
};
|
||||
@@ -1,603 +0,0 @@
|
||||
const { sequelize } = require('../config/database');
|
||||
const responseFormat = require('../utils/response');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 监管任务结项控制器
|
||||
* 处理监管任务结项相关的业务逻辑
|
||||
*/
|
||||
|
||||
// 获取监管任务结项列表
|
||||
const getTaskCompletions = async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
application_no, // 申请编号
|
||||
policy_no, // 保单号
|
||||
product_name, // 产品名称
|
||||
customer_name, // 客户名称
|
||||
completion_date, // 结项日期
|
||||
status, // 状态
|
||||
reviewer_name, // 审核人员
|
||||
page = 1,
|
||||
limit = 10
|
||||
} = req.query;
|
||||
|
||||
// 构建查询条件
|
||||
let whereClause = '';
|
||||
const params = [];
|
||||
|
||||
if (application_no) {
|
||||
whereClause += ' AND rtc.application_no LIKE ?';
|
||||
params.push(`%${application_no}%`);
|
||||
}
|
||||
|
||||
if (policy_no) {
|
||||
whereClause += ' AND rtc.policy_no LIKE ?';
|
||||
params.push(`%${policy_no}%`);
|
||||
}
|
||||
|
||||
if (product_name) {
|
||||
whereClause += ' AND rtc.product_name LIKE ?';
|
||||
params.push(`%${product_name}%`);
|
||||
}
|
||||
|
||||
if (customer_name) {
|
||||
whereClause += ' AND rtc.customer_name LIKE ?';
|
||||
params.push(`%${customer_name}%`);
|
||||
}
|
||||
|
||||
if (completion_date) {
|
||||
whereClause += ' AND DATE(rtc.completion_date) = ?';
|
||||
params.push(completion_date);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND rtc.status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (reviewer_name) {
|
||||
whereClause += ' AND rtc.reviewer_name LIKE ?';
|
||||
params.push(`%${reviewer_name}%`);
|
||||
}
|
||||
|
||||
// 计算偏移量
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 查询总数
|
||||
const countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM regulatory_task_completions rtc
|
||||
WHERE 1=1 ${whereClause}
|
||||
`;
|
||||
|
||||
const [countResult] = await sequelize.query(countQuery, {
|
||||
replacements: params,
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
// 查询数据
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
rtc.id,
|
||||
rtc.application_no,
|
||||
rtc.policy_no,
|
||||
rtc.product_name,
|
||||
rtc.customer_name,
|
||||
rtc.customer_phone,
|
||||
rtc.insurance_amount,
|
||||
rtc.premium_amount,
|
||||
rtc.start_date,
|
||||
rtc.end_date,
|
||||
rtc.completion_date,
|
||||
rtc.status,
|
||||
rtc.reviewer_name,
|
||||
rtc.review_comments,
|
||||
rtc.created_at,
|
||||
rtc.updated_at
|
||||
FROM regulatory_task_completions rtc
|
||||
WHERE 1=1 ${whereClause}
|
||||
ORDER BY rtc.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const results = await sequelize.query(dataQuery, {
|
||||
replacements: [...params, parseInt(limit), offset],
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
res.json(responseFormat.success({
|
||||
list: results,
|
||||
pagination: {
|
||||
current: parseInt(page),
|
||||
pageSize: parseInt(limit),
|
||||
total: countResult.total,
|
||||
totalPages: Math.ceil(countResult.total / limit)
|
||||
}
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取监管任务结项列表失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取监管任务结项列表失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取单个监管任务结项详情
|
||||
const getTaskCompletionById = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
rtc.*,
|
||||
(
|
||||
SELECT JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'id', rtca.id,
|
||||
'file_name', rtca.file_name,
|
||||
'file_path', rtca.file_path,
|
||||
'file_size', rtca.file_size,
|
||||
'file_type', rtca.file_type,
|
||||
'upload_time', rtca.upload_time
|
||||
)
|
||||
)
|
||||
FROM regulatory_task_completion_attachments rtca
|
||||
WHERE rtca.completion_id = rtc.id
|
||||
) as attachments,
|
||||
(
|
||||
SELECT JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'id', rtcl.id,
|
||||
'operation_type', rtcl.operation_type,
|
||||
'operation_description', rtcl.operation_description,
|
||||
'operator_name', rtcl.operator_name,
|
||||
'operation_time', rtcl.operation_time,
|
||||
'ip_address', rtcl.ip_address
|
||||
)
|
||||
)
|
||||
FROM regulatory_task_completion_logs rtcl
|
||||
WHERE rtcl.completion_id = rtc.id
|
||||
ORDER BY rtcl.operation_time DESC
|
||||
) as operation_logs
|
||||
FROM regulatory_task_completions rtc
|
||||
WHERE rtc.id = ?
|
||||
`;
|
||||
|
||||
const [result] = await sequelize.query(query, {
|
||||
replacements: [id],
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return res.status(404).json(responseFormat.error('监管任务结项记录不存在'));
|
||||
}
|
||||
|
||||
// 解析JSON字段
|
||||
if (result.attachments) {
|
||||
result.attachments = JSON.parse(result.attachments) || [];
|
||||
} else {
|
||||
result.attachments = [];
|
||||
}
|
||||
|
||||
if (result.operation_logs) {
|
||||
result.operation_logs = JSON.parse(result.operation_logs) || [];
|
||||
} else {
|
||||
result.operation_logs = [];
|
||||
}
|
||||
|
||||
res.json(responseFormat.success(result));
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取监管任务结项详情失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取监管任务结项详情失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 创建监管任务结项记录
|
||||
const createTaskCompletion = async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
const {
|
||||
application_no,
|
||||
policy_no,
|
||||
product_name,
|
||||
customer_name,
|
||||
customer_phone,
|
||||
insurance_amount,
|
||||
premium_amount,
|
||||
start_date,
|
||||
end_date,
|
||||
completion_date,
|
||||
status = 'pending',
|
||||
review_comments,
|
||||
attachments = []
|
||||
} = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!application_no || !policy_no || !product_name || !customer_name) {
|
||||
return res.status(400).json(responseFormat.error('申请编号、保单号、产品名称、客户名称为必填项'));
|
||||
}
|
||||
|
||||
// 检查申请编号是否已存在
|
||||
const existingQuery = `
|
||||
SELECT id FROM regulatory_task_completions
|
||||
WHERE application_no = ? OR policy_no = ?
|
||||
`;
|
||||
|
||||
const [existing] = await sequelize.query(existingQuery, {
|
||||
replacements: [application_no, policy_no],
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
transaction
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json(responseFormat.error('申请编号或保单号已存在'));
|
||||
}
|
||||
|
||||
// 创建监管任务结项记录
|
||||
const insertQuery = `
|
||||
INSERT INTO regulatory_task_completions (
|
||||
application_no, policy_no, product_name, customer_name, customer_phone,
|
||||
insurance_amount, premium_amount, start_date, end_date, completion_date,
|
||||
status, review_comments, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`;
|
||||
|
||||
const [insertResult] = await sequelize.query(insertQuery, {
|
||||
replacements: [
|
||||
application_no, policy_no, product_name, customer_name, customer_phone,
|
||||
insurance_amount, premium_amount, start_date, end_date, completion_date,
|
||||
status, review_comments
|
||||
],
|
||||
type: sequelize.QueryTypes.INSERT,
|
||||
transaction
|
||||
});
|
||||
|
||||
const completionId = insertResult;
|
||||
|
||||
// 创建附件记录
|
||||
if (attachments && attachments.length > 0) {
|
||||
for (const attachment of attachments) {
|
||||
const attachmentQuery = `
|
||||
INSERT INTO regulatory_task_completion_attachments (
|
||||
completion_id, file_name, file_path, file_size, file_type, upload_time
|
||||
) VALUES (?, ?, ?, ?, ?, NOW())
|
||||
`;
|
||||
|
||||
await sequelize.query(attachmentQuery, {
|
||||
replacements: [
|
||||
completionId,
|
||||
attachment.file_name,
|
||||
attachment.file_path,
|
||||
attachment.file_size,
|
||||
attachment.file_type
|
||||
],
|
||||
type: sequelize.QueryTypes.INSERT,
|
||||
transaction
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
const logQuery = `
|
||||
INSERT INTO regulatory_task_completion_logs (
|
||||
completion_id, operation_type, operation_description,
|
||||
operator_name, operation_time, ip_address
|
||||
) VALUES (?, ?, ?, ?, NOW(), ?)
|
||||
`;
|
||||
|
||||
await sequelize.query(logQuery, {
|
||||
replacements: [
|
||||
completionId,
|
||||
'create',
|
||||
'创建监管任务结项记录',
|
||||
req.user?.real_name || '系统',
|
||||
req.ip
|
||||
],
|
||||
type: sequelize.QueryTypes.INSERT,
|
||||
transaction
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.status(201).json(responseFormat.success({
|
||||
id: completionId,
|
||||
message: '监管任务结项记录创建成功'
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('创建监管任务结项记录失败:', error);
|
||||
res.status(500).json(responseFormat.error('创建监管任务结项记录失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新监管任务结项记录
|
||||
const updateTaskCompletion = async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
application_no,
|
||||
policy_no,
|
||||
product_name,
|
||||
customer_name,
|
||||
customer_phone,
|
||||
insurance_amount,
|
||||
premium_amount,
|
||||
start_date,
|
||||
end_date,
|
||||
completion_date,
|
||||
status,
|
||||
review_comments
|
||||
} = req.body;
|
||||
|
||||
// 检查记录是否存在
|
||||
const checkQuery = `SELECT id FROM regulatory_task_completions WHERE id = ?`;
|
||||
const [existing] = await sequelize.query(checkQuery, {
|
||||
replacements: [id],
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
transaction
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json(responseFormat.error('监管任务结项记录不存在'));
|
||||
}
|
||||
|
||||
// 更新记录
|
||||
const updateQuery = `
|
||||
UPDATE regulatory_task_completions SET
|
||||
application_no = ?, policy_no = ?, product_name = ?, customer_name = ?,
|
||||
customer_phone = ?, insurance_amount = ?, premium_amount = ?,
|
||||
start_date = ?, end_date = ?, completion_date = ?, status = ?,
|
||||
review_comments = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
await sequelize.query(updateQuery, {
|
||||
replacements: [
|
||||
application_no, policy_no, product_name, customer_name, customer_phone,
|
||||
insurance_amount, premium_amount, start_date, end_date, completion_date,
|
||||
status, review_comments, id
|
||||
],
|
||||
type: sequelize.QueryTypes.UPDATE,
|
||||
transaction
|
||||
});
|
||||
|
||||
// 记录操作日志
|
||||
const logQuery = `
|
||||
INSERT INTO regulatory_task_completion_logs (
|
||||
completion_id, operation_type, operation_description,
|
||||
operator_name, operation_time, ip_address
|
||||
) VALUES (?, ?, ?, ?, NOW(), ?)
|
||||
`;
|
||||
|
||||
await sequelize.query(logQuery, {
|
||||
replacements: [
|
||||
id,
|
||||
'update',
|
||||
'更新监管任务结项记录',
|
||||
req.user?.real_name || '系统',
|
||||
req.ip
|
||||
],
|
||||
type: sequelize.QueryTypes.INSERT,
|
||||
transaction
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json(responseFormat.success({ message: '监管任务结项记录更新成功' }));
|
||||
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('更新监管任务结项记录失败:', error);
|
||||
res.status(500).json(responseFormat.error('更新监管任务结项记录失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 删除监管任务结项记录
|
||||
const deleteTaskCompletion = async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 检查记录是否存在
|
||||
const checkQuery = `SELECT id FROM regulatory_task_completions WHERE id = ?`;
|
||||
const [existing] = await sequelize.query(checkQuery, {
|
||||
replacements: [id],
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
transaction
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json(responseFormat.error('监管任务结项记录不存在'));
|
||||
}
|
||||
|
||||
// 删除相关附件记录
|
||||
await sequelize.query(
|
||||
'DELETE FROM regulatory_task_completion_attachments WHERE completion_id = ?',
|
||||
{
|
||||
replacements: [id],
|
||||
type: sequelize.QueryTypes.DELETE,
|
||||
transaction
|
||||
}
|
||||
);
|
||||
|
||||
// 删除相关日志记录
|
||||
await sequelize.query(
|
||||
'DELETE FROM regulatory_task_completion_logs WHERE completion_id = ?',
|
||||
{
|
||||
replacements: [id],
|
||||
type: sequelize.QueryTypes.DELETE,
|
||||
transaction
|
||||
}
|
||||
);
|
||||
|
||||
// 删除主记录
|
||||
await sequelize.query(
|
||||
'DELETE FROM regulatory_task_completions WHERE id = ?',
|
||||
{
|
||||
replacements: [id],
|
||||
type: sequelize.QueryTypes.DELETE,
|
||||
transaction
|
||||
}
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json(responseFormat.success({ message: '监管任务结项记录删除成功' }));
|
||||
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('删除监管任务结项记录失败:', error);
|
||||
res.status(500).json(responseFormat.error('删除监管任务结项记录失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 审核监管任务结项记录
|
||||
const reviewTaskCompletion = async (req, res) => {
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { status, review_comments, reviewer_name } = req.body;
|
||||
|
||||
// 验证状态值
|
||||
const validStatuses = ['pending', 'approved', 'rejected', 'completed'];
|
||||
if (!validStatuses.includes(status)) {
|
||||
return res.status(400).json(responseFormat.error('无效的状态值'));
|
||||
}
|
||||
|
||||
// 检查记录是否存在
|
||||
const checkQuery = `SELECT id, status FROM regulatory_task_completions WHERE id = ?`;
|
||||
const [existing] = await sequelize.query(checkQuery, {
|
||||
replacements: [id],
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
transaction
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json(responseFormat.error('监管任务结项记录不存在'));
|
||||
}
|
||||
|
||||
// 更新审核状态
|
||||
const updateQuery = `
|
||||
UPDATE regulatory_task_completions SET
|
||||
status = ?, review_comments = ?, reviewer_name = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
await sequelize.query(updateQuery, {
|
||||
replacements: [status, review_comments, reviewer_name, id],
|
||||
type: sequelize.QueryTypes.UPDATE,
|
||||
transaction
|
||||
});
|
||||
|
||||
// 记录操作日志
|
||||
const logQuery = `
|
||||
INSERT INTO regulatory_task_completion_logs (
|
||||
completion_id, operation_type, operation_description,
|
||||
operator_name, operation_time, ip_address
|
||||
) VALUES (?, ?, ?, ?, NOW(), ?)
|
||||
`;
|
||||
|
||||
const operationDesc = `审核监管任务结项记录,状态变更为:${status}`;
|
||||
await sequelize.query(logQuery, {
|
||||
replacements: [
|
||||
id,
|
||||
'review',
|
||||
operationDesc,
|
||||
reviewer_name || req.user?.real_name || '系统',
|
||||
req.ip
|
||||
],
|
||||
type: sequelize.QueryTypes.INSERT,
|
||||
transaction
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json(responseFormat.success({ message: '监管任务结项记录审核成功' }));
|
||||
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('审核监管任务结项记录失败:', error);
|
||||
res.status(500).json(responseFormat.error('审核监管任务结项记录失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取监管任务结项统计数据
|
||||
const getTaskCompletionStats = async (req, res) => {
|
||||
try {
|
||||
// 状态统计
|
||||
const statusStatsQuery = `
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count
|
||||
FROM regulatory_task_completions
|
||||
GROUP BY status
|
||||
`;
|
||||
|
||||
const statusStats = await sequelize.query(statusStatsQuery, {
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
// 月度统计
|
||||
const monthlyStatsQuery = `
|
||||
SELECT
|
||||
DATE_FORMAT(completion_date, '%Y-%m') as month,
|
||||
COUNT(*) as count,
|
||||
SUM(insurance_amount) as total_amount
|
||||
FROM regulatory_task_completions
|
||||
WHERE completion_date >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||
GROUP BY DATE_FORMAT(completion_date, '%Y-%m')
|
||||
ORDER BY month
|
||||
`;
|
||||
|
||||
const monthlyStats = await sequelize.query(monthlyStatsQuery, {
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
// 产品统计
|
||||
const productStatsQuery = `
|
||||
SELECT
|
||||
product_name,
|
||||
COUNT(*) as count,
|
||||
SUM(insurance_amount) as total_amount
|
||||
FROM regulatory_task_completions
|
||||
GROUP BY product_name
|
||||
ORDER BY count DESC
|
||||
LIMIT 10
|
||||
`;
|
||||
|
||||
const productStats = await sequelize.query(productStatsQuery, {
|
||||
type: sequelize.QueryTypes.SELECT
|
||||
});
|
||||
|
||||
res.json(responseFormat.success({
|
||||
statusStats,
|
||||
monthlyStats,
|
||||
productStats
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取监管任务结项统计数据失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取监管任务结项统计数据失败'));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getTaskCompletions,
|
||||
getTaskCompletionById,
|
||||
createTaskCompletion,
|
||||
updateTaskCompletion,
|
||||
deleteTaskCompletion,
|
||||
reviewTaskCompletion,
|
||||
getTaskCompletionStats
|
||||
};
|
||||
@@ -1,459 +0,0 @@
|
||||
const { Role, Permission, RolePermission, User } = require('../models');
|
||||
const responseFormat = require('../utils/response');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 角色权限管理控制器
|
||||
* 专门处理角色权限的分配、管理和动态调用
|
||||
*/
|
||||
class RolePermissionController {
|
||||
|
||||
/**
|
||||
* 获取所有角色及其权限
|
||||
*/
|
||||
async getAllRolesWithPermissions(req, res) {
|
||||
try {
|
||||
const roles = await Role.findAll({
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
|
||||
const rolesData = await Promise.all(roles.map(async (role) => {
|
||||
// 从RolePermission表获取权限
|
||||
const rolePermissions = await RolePermission.findAll({
|
||||
where: {
|
||||
role_id: role.id,
|
||||
granted: true
|
||||
},
|
||||
include: [{
|
||||
model: Permission,
|
||||
as: 'permission',
|
||||
attributes: ['id', 'name', 'code', 'description', 'module', 'type']
|
||||
}]
|
||||
});
|
||||
|
||||
const permissions = rolePermissions.map(rp => rp.permission.code);
|
||||
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
status: role.status,
|
||||
permissions: permissions,
|
||||
permissionCount: permissions.length
|
||||
};
|
||||
}));
|
||||
|
||||
res.json(responseFormat.success({
|
||||
roles: rolesData,
|
||||
total: rolesData.length
|
||||
}, '获取角色权限列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取角色权限列表失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取角色权限列表失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有权限
|
||||
*/
|
||||
async getAllPermissions(req, res) {
|
||||
try {
|
||||
const permissions = await Permission.findAll({
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
|
||||
res.json(responseFormat.success(permissions, '获取权限列表成功'));
|
||||
} catch (error) {
|
||||
console.error('获取权限列表失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取权限列表失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定角色的权限详情
|
||||
*/
|
||||
async getRolePermissionDetail(req, res) {
|
||||
try {
|
||||
const { roleId } = req.params;
|
||||
console.log('获取角色权限详情,角色ID:', roleId);
|
||||
|
||||
const role = await Role.findByPk(roleId);
|
||||
console.log('角色查询结果:', role ? role.name : '未找到');
|
||||
|
||||
if (!role) {
|
||||
return res.status(404).json(responseFormat.error('角色不存在'));
|
||||
}
|
||||
|
||||
// 获取所有权限用于对比
|
||||
const allPermissions = await Permission.findAll({
|
||||
attributes: ['id', 'name', 'code', 'description', 'module', 'type', 'parent_id'],
|
||||
order: [['module', 'ASC'], ['id', 'ASC']]
|
||||
});
|
||||
console.log('权限查询结果:', allPermissions.length, '个权限');
|
||||
|
||||
// 从RolePermission表获取角色已分配的权限
|
||||
const rolePermissions = await RolePermission.findAll({
|
||||
where: {
|
||||
role_id: roleId,
|
||||
granted: true
|
||||
},
|
||||
include: [{
|
||||
model: Permission,
|
||||
as: 'permission',
|
||||
attributes: ['id', 'name', 'code', 'description', 'module', 'type']
|
||||
}]
|
||||
});
|
||||
|
||||
const assignedPermissionCodes = rolePermissions.map(rp => rp.permission.code);
|
||||
console.log('已分配权限代码:', assignedPermissionCodes.length, '个');
|
||||
|
||||
// 暂时返回简化数据,不构建权限树
|
||||
res.json(responseFormat.success({
|
||||
role: {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
description: role.description,
|
||||
status: role.status
|
||||
},
|
||||
assignedPermissions: assignedPermissionCodes,
|
||||
allPermissions: allPermissions.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
code: p.code,
|
||||
description: p.description,
|
||||
module: p.module,
|
||||
type: p.type,
|
||||
parent_id: p.parent_id,
|
||||
assigned: assignedPermissionCodes.includes(p.code)
|
||||
})),
|
||||
assignedCount: assignedPermissionCodes.length,
|
||||
totalCount: allPermissions.length
|
||||
}, '获取角色权限详情成功'));
|
||||
} catch (error) {
|
||||
console.error('获取角色权限详情失败:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
res.status(500).json(responseFormat.error('获取角色权限详情失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量分配角色权限
|
||||
*/
|
||||
async batchAssignPermissions(req, res) {
|
||||
try {
|
||||
const { roleId } = req.params;
|
||||
const { permissionIds, operation = 'replace' } = req.body;
|
||||
|
||||
console.log('=== 批量分配权限开始 ===');
|
||||
console.log('角色ID:', roleId);
|
||||
console.log('权限ID列表:', permissionIds);
|
||||
console.log('操作类型:', operation);
|
||||
console.log('请求体:', JSON.stringify(req.body, null, 2));
|
||||
|
||||
if (!Array.isArray(permissionIds)) {
|
||||
console.log('❌ 权限ID列表格式错误');
|
||||
return res.status(400).json(responseFormat.error('权限ID列表格式错误'));
|
||||
}
|
||||
|
||||
const role = await Role.findByPk(roleId);
|
||||
if (!role) {
|
||||
console.log('❌ 角色不存在');
|
||||
return res.status(404).json(responseFormat.error('角色不存在'));
|
||||
}
|
||||
|
||||
console.log('找到角色:', role.name);
|
||||
|
||||
// 验证权限ID是否存在
|
||||
const validPermissions = await Permission.findAll({
|
||||
where: { id: { [Op.in]: permissionIds } },
|
||||
attributes: ['id']
|
||||
});
|
||||
|
||||
const validPermissionIds = validPermissions.map(p => p.id);
|
||||
const invalidIds = permissionIds.filter(id => !validPermissionIds.includes(id));
|
||||
|
||||
console.log('有效权限ID:', validPermissionIds);
|
||||
console.log('无效权限ID:', invalidIds);
|
||||
|
||||
if (invalidIds.length > 0) {
|
||||
console.log('❌ 存在无效的权限ID');
|
||||
return res.status(400).json(responseFormat.error(`无效的权限ID: ${invalidIds.join(', ')}`));
|
||||
}
|
||||
|
||||
// 根据操作类型处理权限分配
|
||||
if (operation === 'replace') {
|
||||
console.log('执行替换模式权限分配');
|
||||
|
||||
// 替换模式:删除现有权限,添加新权限
|
||||
const deletedCount = await RolePermission.destroy({ where: { role_id: roleId } });
|
||||
console.log('删除现有权限数量:', deletedCount);
|
||||
|
||||
if (permissionIds.length > 0) {
|
||||
const rolePermissions = permissionIds.map(permissionId => ({
|
||||
role_id: roleId,
|
||||
permission_id: permissionId,
|
||||
granted: true
|
||||
}));
|
||||
console.log('准备创建的权限记录:', rolePermissions);
|
||||
|
||||
const createdPermissions = await RolePermission.bulkCreate(rolePermissions);
|
||||
console.log('成功创建的权限记录数量:', createdPermissions.length);
|
||||
}
|
||||
} else if (operation === 'add') {
|
||||
// 添加模式:只添加新权限
|
||||
const existingPermissions = await RolePermission.findAll({
|
||||
where: { role_id: roleId },
|
||||
attributes: ['permission_id']
|
||||
});
|
||||
const existingIds = existingPermissions.map(p => p.permission_id);
|
||||
const newPermissionIds = permissionIds.filter(id => !existingIds.includes(id));
|
||||
|
||||
if (newPermissionIds.length > 0) {
|
||||
const rolePermissions = newPermissionIds.map(permissionId => ({
|
||||
role_id: roleId,
|
||||
permission_id: permissionId,
|
||||
granted: true
|
||||
}));
|
||||
await RolePermission.bulkCreate(rolePermissions);
|
||||
}
|
||||
} else if (operation === 'remove') {
|
||||
// 移除模式:删除指定权限
|
||||
await RolePermission.destroy({
|
||||
where: {
|
||||
role_id: roleId,
|
||||
permission_id: { [Op.in]: permissionIds }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ 权限分配完成');
|
||||
res.json(responseFormat.success(null, `${operation === 'replace' ? '替换' : operation === 'add' ? '添加' : '移除'}角色权限成功`));
|
||||
} catch (error) {
|
||||
console.error('❌ 批量分配权限失败:', error);
|
||||
console.error('错误堆栈:', error.stack);
|
||||
res.status(500).json(responseFormat.error('批量分配角色权限失败'));
|
||||
}
|
||||
|
||||
console.log('=== 批量分配权限结束 ===');
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制角色权限
|
||||
*/
|
||||
async copyRolePermissions(req, res) {
|
||||
try {
|
||||
const { sourceRoleId, targetRoleId } = req.body;
|
||||
|
||||
if (!sourceRoleId || !targetRoleId) {
|
||||
return res.status(400).json(responseFormat.error('源角色ID和目标角色ID不能为空'));
|
||||
}
|
||||
|
||||
if (sourceRoleId === targetRoleId) {
|
||||
return res.status(400).json(responseFormat.error('源角色和目标角色不能相同'));
|
||||
}
|
||||
|
||||
// 验证角色存在
|
||||
const [sourceRole, targetRole] = await Promise.all([
|
||||
Role.findByPk(sourceRoleId),
|
||||
Role.findByPk(targetRoleId)
|
||||
]);
|
||||
|
||||
if (!sourceRole) {
|
||||
return res.status(404).json(responseFormat.error('源角色不存在'));
|
||||
}
|
||||
if (!targetRole) {
|
||||
return res.status(404).json(responseFormat.error('目标角色不存在'));
|
||||
}
|
||||
|
||||
// 获取源角色的权限
|
||||
const sourcePermissions = await RolePermission.findAll({
|
||||
where: { role_id: sourceRoleId },
|
||||
attributes: ['permission_id']
|
||||
});
|
||||
|
||||
// 删除目标角色现有权限
|
||||
await RolePermission.destroy({ where: { role_id: targetRoleId } });
|
||||
|
||||
// 复制权限到目标角色
|
||||
if (sourcePermissions.length > 0) {
|
||||
const targetPermissions = sourcePermissions.map(p => ({
|
||||
role_id: targetRoleId,
|
||||
permission_id: p.permission_id,
|
||||
granted: true
|
||||
}));
|
||||
await RolePermission.bulkCreate(targetPermissions);
|
||||
}
|
||||
|
||||
res.json(responseFormat.success(null, `成功将 ${sourceRole.name} 的权限复制到 ${targetRole.name}`));
|
||||
} catch (error) {
|
||||
console.error('复制角色权限失败:', error);
|
||||
res.status(500).json(responseFormat.error('复制角色权限失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户权限
|
||||
*/
|
||||
async checkUserPermission(req, res) {
|
||||
try {
|
||||
const { userId, permissionCode } = req.params;
|
||||
|
||||
const user = await User.findByPk(userId, {
|
||||
include: [
|
||||
{
|
||||
model: Role,
|
||||
as: 'role',
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
as: 'rolePermissions',
|
||||
where: { code: permissionCode },
|
||||
required: false,
|
||||
through: {
|
||||
attributes: ['granted']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json(responseFormat.error('用户不存在'));
|
||||
}
|
||||
|
||||
const hasPermission = user.role &&
|
||||
user.role.rolePermissions &&
|
||||
user.role.rolePermissions.length > 0 &&
|
||||
user.role.rolePermissions[0].RolePermission.granted;
|
||||
|
||||
res.json(responseFormat.success({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
roleName: user.role ? user.role.name : null,
|
||||
permissionCode,
|
||||
hasPermission,
|
||||
checkTime: new Date()
|
||||
}, '权限检查完成'));
|
||||
} catch (error) {
|
||||
console.error('检查用户权限失败:', error);
|
||||
res.status(500).json(responseFormat.error('检查用户权限失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限统计信息
|
||||
*/
|
||||
async getPermissionStats(req, res) {
|
||||
try {
|
||||
// 统计各种数据
|
||||
const [
|
||||
totalRoles,
|
||||
totalPermissions,
|
||||
moduleStats,
|
||||
roles
|
||||
] = await Promise.all([
|
||||
Role.count(),
|
||||
Permission.count(),
|
||||
Permission.findAll({
|
||||
attributes: [
|
||||
'module',
|
||||
[Permission.sequelize.fn('COUNT', Permission.sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['module'],
|
||||
order: [['module', 'ASC']]
|
||||
}),
|
||||
Role.findAll({
|
||||
attributes: ['id', 'name', 'permissions'],
|
||||
order: [['name', 'ASC']]
|
||||
})
|
||||
]);
|
||||
|
||||
// 计算总分配数和角色权限分布
|
||||
let totalAssignments = 0;
|
||||
const roleDistribution = roles.map(role => {
|
||||
let permissions = [];
|
||||
if (Array.isArray(role.permissions)) {
|
||||
permissions = role.permissions;
|
||||
} else if (typeof role.permissions === 'string') {
|
||||
try {
|
||||
permissions = JSON.parse(role.permissions);
|
||||
} catch (e) {
|
||||
permissions = [];
|
||||
}
|
||||
}
|
||||
|
||||
const permissionCount = permissions.length;
|
||||
totalAssignments += permissionCount;
|
||||
return {
|
||||
roleId: role.id,
|
||||
roleName: role.name,
|
||||
permissionCount
|
||||
};
|
||||
});
|
||||
|
||||
res.json(responseFormat.success({
|
||||
overview: {
|
||||
totalRoles,
|
||||
totalPermissions,
|
||||
totalAssignments,
|
||||
averagePermissionsPerRole: totalRoles > 0 ? Math.round(totalAssignments / totalRoles) : 0
|
||||
},
|
||||
moduleDistribution: moduleStats.map(stat => ({
|
||||
module: stat.module,
|
||||
count: parseInt(stat.dataValues.count)
|
||||
})),
|
||||
roleDistribution
|
||||
}, '获取权限统计成功'));
|
||||
} catch (error) {
|
||||
console.error('获取权限统计失败:', error);
|
||||
res.status(500).json(responseFormat.error('获取权限统计失败'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建权限树
|
||||
*/
|
||||
buildPermissionTree(permissions, parentId = null) {
|
||||
const tree = [];
|
||||
for (const permission of permissions) {
|
||||
if (permission.parent_id === parentId) {
|
||||
const children = this.buildPermissionTree(permissions, permission.id);
|
||||
const node = {
|
||||
...(permission.toJSON ? permission.toJSON() : permission),
|
||||
children: children.length > 0 ? children : undefined
|
||||
};
|
||||
tree.push(node);
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记已分配的权限
|
||||
*/
|
||||
markAssignedPermissions(permissions, assignedIds) {
|
||||
return permissions.map(permission => ({
|
||||
...permission,
|
||||
assigned: assignedIds.includes(permission.id),
|
||||
children: permission.children ?
|
||||
this.markAssignedPermissions(permission.children, assignedIds) :
|
||||
undefined
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据权限代码标记已分配的权限
|
||||
*/
|
||||
markAssignedPermissionsByCode(permissions, assignedCodes) {
|
||||
return permissions.map(permission => ({
|
||||
...permission,
|
||||
assigned: assignedCodes.includes(permission.code),
|
||||
children: permission.children ?
|
||||
this.markAssignedPermissionsByCode(permission.children, assignedCodes) :
|
||||
undefined
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new RolePermissionController();
|
||||
@@ -1,316 +0,0 @@
|
||||
const { User, Role, InsuranceApplication, Policy, Claim } = require('../models');
|
||||
const responseFormat = require('../utils/response');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// 获取系统统计信息
|
||||
const getSystemStats = async (req, res) => {
|
||||
try {
|
||||
const [
|
||||
totalUsers,
|
||||
totalRoles,
|
||||
totalApplications,
|
||||
totalPolicies,
|
||||
totalClaims,
|
||||
activeUsers,
|
||||
pendingApplications,
|
||||
approvedApplications,
|
||||
activePolicies,
|
||||
pendingClaims,
|
||||
approvedClaims
|
||||
] = await Promise.all([
|
||||
User.count(),
|
||||
Role.count(),
|
||||
InsuranceApplication.count(),
|
||||
Policy.count(),
|
||||
Claim.count(),
|
||||
User.count({ where: { status: 'active' } }),
|
||||
InsuranceApplication.count({ where: { status: 'pending' } }),
|
||||
InsuranceApplication.count({ where: { status: 'approved' } }),
|
||||
Policy.count({ where: { policy_status: 'active' } }),
|
||||
Claim.count({ where: { claim_status: 'pending' } }),
|
||||
Claim.count({ where: { claim_status: 'approved' } })
|
||||
]);
|
||||
|
||||
// 获取最近7天的数据趋势
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
|
||||
const recentStats = await Promise.all([
|
||||
User.count({ where: { created_at: { [Op.gte]: sevenDaysAgo } } }),
|
||||
InsuranceApplication.count({ where: { created_at: { [Op.gte]: sevenDaysAgo } } }),
|
||||
Policy.count({ where: { created_at: { [Op.gte]: sevenDaysAgo } } }),
|
||||
Claim.count({ where: { created_at: { [Op.gte]: sevenDaysAgo } } })
|
||||
]);
|
||||
|
||||
res.json(responseFormat.success({
|
||||
overview: {
|
||||
users: totalUsers,
|
||||
roles: totalRoles,
|
||||
applications: totalApplications,
|
||||
policies: totalPolicies,
|
||||
claims: totalClaims
|
||||
},
|
||||
status: {
|
||||
active_users: activeUsers,
|
||||
pending_applications: pendingApplications,
|
||||
approved_applications: approvedApplications,
|
||||
active_policies: activePolicies,
|
||||
pending_claims: pendingClaims,
|
||||
approved_claims: approvedClaims
|
||||
},
|
||||
recent: {
|
||||
new_users: recentStats[0],
|
||||
new_applications: recentStats[1],
|
||||
new_policies: recentStats[2],
|
||||
new_claims: recentStats[3]
|
||||
}
|
||||
}, '获取系统统计信息成功'));
|
||||
} catch (error) {
|
||||
console.error('获取系统统计信息错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取系统统计信息失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取系统日志(模拟)
|
||||
const getSystemLogs = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 50, level, start_date, end_date } = req.query;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// 模拟日志数据 - 扩展更多有意义的日志记录
|
||||
const mockLogs = [
|
||||
{
|
||||
id: 1,
|
||||
level: 'info',
|
||||
message: '系统启动成功',
|
||||
timestamp: new Date().toISOString(),
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
level: 'info',
|
||||
message: '数据库连接成功',
|
||||
timestamp: new Date(Date.now() - 1000 * 60).toISOString(),
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
level: 'warning',
|
||||
message: 'Redis连接失败,使用内存缓存',
|
||||
timestamp: new Date(Date.now() - 1000 * 120).toISOString(),
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
level: 'info',
|
||||
message: '用户 admin 登录成功',
|
||||
timestamp: new Date(Date.now() - 1000 * 180).toISOString(),
|
||||
user: 'admin'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
level: 'info',
|
||||
message: '新增保险申请:车险申请 - 申请人:张三',
|
||||
timestamp: new Date(Date.now() - 1000 * 240).toISOString(),
|
||||
user: 'zhangsan'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
level: 'info',
|
||||
message: '保单生效:保单号 POL-2024-001 - 投保人:李四',
|
||||
timestamp: new Date(Date.now() - 1000 * 300).toISOString(),
|
||||
user: 'lisi'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
level: 'warning',
|
||||
message: '理赔申请待审核:理赔号 CLM-2024-001 - 申请人:王五',
|
||||
timestamp: new Date(Date.now() - 1000 * 360).toISOString(),
|
||||
user: 'wangwu'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
level: 'info',
|
||||
message: '新用户注册:用户名 zhaoliu',
|
||||
timestamp: new Date(Date.now() - 1000 * 420).toISOString(),
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
level: 'error',
|
||||
message: '支付接口调用失败:订单号 ORD-2024-001',
|
||||
timestamp: new Date(Date.now() - 1000 * 480).toISOString(),
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
level: 'info',
|
||||
message: '保险类型更新:新增意外险产品',
|
||||
timestamp: new Date(Date.now() - 1000 * 540).toISOString(),
|
||||
user: 'admin'
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
level: 'info',
|
||||
message: '系统备份完成:数据库备份成功',
|
||||
timestamp: new Date(Date.now() - 1000 * 600).toISOString(),
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
level: 'warning',
|
||||
message: '磁盘空间不足警告:剩余空间 15%',
|
||||
timestamp: new Date(Date.now() - 1000 * 660).toISOString(),
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
level: 'info',
|
||||
message: '理赔审核通过:理赔号 CLM-2024-002 - 赔付金额 ¥5000',
|
||||
timestamp: new Date(Date.now() - 1000 * 720).toISOString(),
|
||||
user: 'admin'
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
level: 'info',
|
||||
message: '保单续费成功:保单号 POL-2024-002 - 续费期限 1年',
|
||||
timestamp: new Date(Date.now() - 1000 * 780).toISOString(),
|
||||
user: 'system'
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
level: 'error',
|
||||
message: '短信发送失败:手机号 138****8888',
|
||||
timestamp: new Date(Date.now() - 1000 * 840).toISOString(),
|
||||
user: 'system'
|
||||
}
|
||||
];
|
||||
|
||||
// 简单的过滤逻辑
|
||||
let filteredLogs = mockLogs;
|
||||
if (level) {
|
||||
filteredLogs = filteredLogs.filter(log => log.level === level);
|
||||
}
|
||||
if (start_date) {
|
||||
const startDate = new Date(start_date);
|
||||
filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) >= startDate);
|
||||
}
|
||||
if (end_date) {
|
||||
const endDate = new Date(end_date);
|
||||
filteredLogs = filteredLogs.filter(log => new Date(log.timestamp) <= endDate);
|
||||
}
|
||||
|
||||
// 分页
|
||||
const paginatedLogs = filteredLogs.slice(offset, offset + parseInt(limit));
|
||||
|
||||
res.json(responseFormat.success({
|
||||
logs: paginatedLogs,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total: filteredLogs.length,
|
||||
pages: Math.ceil(filteredLogs.length / parseInt(limit))
|
||||
}
|
||||
}, '获取系统日志成功'));
|
||||
} catch (error) {
|
||||
console.error('获取系统日志错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取系统日志失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取系统配置(模拟)
|
||||
const getSystemConfig = async (req, res) => {
|
||||
try {
|
||||
const config = {
|
||||
site_name: '保险端口管理系统',
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
max_file_size: '10MB',
|
||||
allowed_file_types: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
|
||||
session_timeout: 3600,
|
||||
backup_enabled: true,
|
||||
backup_schedule: '0 2 * * *', // 每天凌晨2点
|
||||
email_notifications: true,
|
||||
sms_notifications: false,
|
||||
maintenance_mode: false
|
||||
};
|
||||
|
||||
res.json(responseFormat.success(config, '获取系统配置成功'));
|
||||
} catch (error) {
|
||||
console.error('获取系统配置错误:', error);
|
||||
res.status(500).json(responseFormat.error('获取系统配置失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 更新系统配置(模拟)
|
||||
const updateSystemConfig = async (req, res) => {
|
||||
try {
|
||||
const { maintenance_mode, email_notifications, sms_notifications } = req.body;
|
||||
|
||||
// 模拟更新配置
|
||||
const updatedConfig = {
|
||||
maintenance_mode: maintenance_mode !== undefined ? maintenance_mode : false,
|
||||
email_notifications: email_notifications !== undefined ? email_notifications : true,
|
||||
sms_notifications: sms_notifications !== undefined ? sms_notifications : false,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(responseFormat.success(updatedConfig, '系统配置更新成功'));
|
||||
} catch (error) {
|
||||
console.error('更新系统配置错误:', error);
|
||||
res.status(500).json(responseFormat.error('更新系统配置失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 备份数据库(模拟)
|
||||
const backupDatabase = async (req, res) => {
|
||||
try {
|
||||
// 模拟备份过程
|
||||
const backupInfo = {
|
||||
id: `backup_${Date.now()}`,
|
||||
filename: `backup_${new Date().toISOString().replace(/:/g, '-')}.sql`,
|
||||
size: '2.5MB',
|
||||
status: 'completed',
|
||||
created_at: new Date().toISOString(),
|
||||
download_url: '/api/system/backup/download/backup.sql'
|
||||
};
|
||||
|
||||
res.json(responseFormat.success(backupInfo, '数据库备份成功'));
|
||||
} catch (error) {
|
||||
console.error('数据库备份错误:', error);
|
||||
res.status(500).json(responseFormat.error('数据库备份失败'));
|
||||
}
|
||||
};
|
||||
|
||||
// 恢复数据库(模拟)
|
||||
const restoreDatabase = async (req, res) => {
|
||||
try {
|
||||
const { backup_id } = req.body;
|
||||
|
||||
if (!backup_id) {
|
||||
return res.status(400).json(responseFormat.error('备份ID不能为空'));
|
||||
}
|
||||
|
||||
// 模拟恢复过程
|
||||
const restoreInfo = {
|
||||
backup_id,
|
||||
status: 'completed',
|
||||
restored_at: new Date().toISOString(),
|
||||
message: '数据库恢复成功'
|
||||
};
|
||||
|
||||
res.json(responseFormat.success(restoreInfo, '数据库恢复成功'));
|
||||
} catch (error) {
|
||||
console.error('数据库恢复错误:', error);
|
||||
res.status(500).json(responseFormat.error('数据库恢复失败'));
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getSystemStats,
|
||||
getSystemLogs,
|
||||
getSystemConfig,
|
||||
updateSystemConfig,
|
||||
backupDatabase,
|
||||
restoreDatabase
|
||||
};
|
||||
@@ -453,8 +453,8 @@ components:
|
||||
example: "针对牛羊等大型牲畜的综合保险产品"
|
||||
applicable_livestock:
|
||||
type: string
|
||||
description: 适用牲畜类型
|
||||
example: "牛、羊"
|
||||
description: 适用牲畜类型,多个类型用逗号分隔。前端可发送数组,后端会自动转换为字符串存储
|
||||
example: "牛,羊,猪"
|
||||
maxLength: 100
|
||||
insurance_term:
|
||||
type: integer
|
||||
@@ -530,8 +530,8 @@ components:
|
||||
example: "针对牛羊等大型牲畜的综合保险产品"
|
||||
applicable_livestock:
|
||||
type: string
|
||||
description: 适用牲畜类型
|
||||
example: "牛、羊"
|
||||
description: 适用牲畜类型,多个类型用逗号分隔。前端可发送数组,后端会自动转换为字符串存储
|
||||
example: "牛,羊,猪"
|
||||
maxLength: 100
|
||||
insurance_term:
|
||||
type: integer
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const User = require('../models/User');
|
||||
const Role = require('../models/Role');
|
||||
const responseFormat = require('../utils/response');
|
||||
|
||||
// JWT认证中间件
|
||||
const jwtAuth = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
console.log('Authorization header:', authHeader);
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json(responseFormat.error('未提供有效的认证token'));
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
console.log('提取的token:', token);
|
||||
console.log('token类型:', typeof token);
|
||||
console.log('token长度:', token.length);
|
||||
|
||||
// 首先尝试固定token验证
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
fixed_token: token,
|
||||
status: 'active'
|
||||
},
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role',
|
||||
attributes: ['id', 'name', 'permissions']
|
||||
}]
|
||||
});
|
||||
|
||||
if (user) {
|
||||
// 固定token验证成功
|
||||
req.user = {
|
||||
id: user.id,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
role_id: user.role_id,
|
||||
role: user.role,
|
||||
permissions: user.role ? user.role.permissions : [],
|
||||
type: 'fixed_token'
|
||||
};
|
||||
return next();
|
||||
}
|
||||
|
||||
// 如果固定token验证失败,尝试JWT验证
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
if (decoded.type !== 'access') {
|
||||
return res.status(401).json(responseFormat.error('Token类型错误'));
|
||||
}
|
||||
|
||||
// 验证用户是否存在且状态正常
|
||||
const jwtUser = await User.findOne({
|
||||
where: {
|
||||
id: decoded.id,
|
||||
status: 'active'
|
||||
},
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role',
|
||||
attributes: ['id', 'name', 'permissions']
|
||||
}]
|
||||
});
|
||||
|
||||
if (!jwtUser) {
|
||||
return res.status(401).json(responseFormat.error('用户不存在或已被禁用'));
|
||||
}
|
||||
|
||||
req.user = {
|
||||
id: decoded.id,
|
||||
userId: decoded.id,
|
||||
username: decoded.username,
|
||||
role_id: decoded.role_id,
|
||||
role: jwtUser.role,
|
||||
permissions: decoded.permissions || (jwtUser.role ? jwtUser.role.permissions : []),
|
||||
type: 'jwt'
|
||||
};
|
||||
|
||||
return next();
|
||||
} catch (jwtError) {
|
||||
if (jwtError.name === 'TokenExpiredError') {
|
||||
return res.status(401).json(responseFormat.error('Token已过期', 'TOKEN_EXPIRED'));
|
||||
} else if (jwtError.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json(responseFormat.error('Token无效'));
|
||||
}
|
||||
throw jwtError;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token验证错误:', error);
|
||||
return res.status(500).json(responseFormat.error('服务器内部错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 权限检查中间件
|
||||
const checkPermission = (resource, action) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
console.log(`权限检查 - 资源: ${resource}, 操作: ${action}, 用户:`, req.user);
|
||||
const user = req.user;
|
||||
|
||||
if (!user || !user.role_id) {
|
||||
console.log('权限检查失败 - 用户角色信息缺失');
|
||||
return res.status(403).json(responseFormat.error('用户角色信息缺失'));
|
||||
}
|
||||
|
||||
let permissions = [];
|
||||
|
||||
// 优先使用JWT中的权限信息
|
||||
if (user.permissions) {
|
||||
if (typeof user.permissions === 'string') {
|
||||
try {
|
||||
permissions = JSON.parse(user.permissions);
|
||||
} catch (e) {
|
||||
console.log('JWT权限解析失败:', e.message);
|
||||
permissions = [];
|
||||
}
|
||||
} else if (Array.isArray(user.permissions)) {
|
||||
permissions = user.permissions;
|
||||
}
|
||||
}
|
||||
|
||||
const requiredPermission = `${resource}:${action}`;
|
||||
|
||||
// 首先检查JWT中的权限
|
||||
let hasPermission = permissions.includes(requiredPermission) ||
|
||||
permissions.includes('*:*') ||
|
||||
permissions.includes('*');
|
||||
|
||||
// 如果JWT中没有权限信息,或者JWT权限不足,从数据库查询最新权限
|
||||
if (permissions.length === 0 || !hasPermission) {
|
||||
console.log('JWT权限不足或为空,从数据库获取最新权限...');
|
||||
const { Role, RolePermission, Permission } = require('../models');
|
||||
const userRole = await Role.findByPk(user.role_id);
|
||||
|
||||
if (!userRole) {
|
||||
console.log('权限检查失败 - 用户角色不存在, role_id:', user.role_id);
|
||||
return res.status(403).json(responseFormat.error('用户角色不存在'));
|
||||
}
|
||||
|
||||
// 从RolePermission表获取权限
|
||||
const rolePermissions = await RolePermission.findAll({
|
||||
where: {
|
||||
role_id: user.role_id,
|
||||
granted: true
|
||||
},
|
||||
include: [{
|
||||
model: Permission,
|
||||
as: 'permission',
|
||||
attributes: ['code']
|
||||
}]
|
||||
});
|
||||
|
||||
permissions = rolePermissions.map(rp => rp.permission.code);
|
||||
console.log('从RolePermission表获取的最新权限:', permissions);
|
||||
|
||||
// 重新检查权限
|
||||
hasPermission = permissions.includes(requiredPermission) ||
|
||||
permissions.includes('*:*') ||
|
||||
permissions.includes('*');
|
||||
}
|
||||
|
||||
console.log('权限检查 - 用户权限:', permissions, '需要权限:', requiredPermission, '是否有权限:', hasPermission);
|
||||
|
||||
// 检查权限或超级管理员权限
|
||||
if (!hasPermission) {
|
||||
console.log('权限检查失败 - 权限不足');
|
||||
return res.status(403).json(responseFormat.error('权限不足'));
|
||||
}
|
||||
|
||||
console.log('权限检查通过');
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(500).json(responseFormat.error('权限验证失败'));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 可选认证中间件(不强制要求认证)
|
||||
const optionalAuth = (req, res, next) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
req.user = decoded;
|
||||
} catch (error) {
|
||||
// 令牌无效,但不阻止请求
|
||||
console.warn('可选认证令牌无效:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// 别名导出以匹配路由中的使用
|
||||
const authenticateToken = jwtAuth;
|
||||
const requirePermission = (permission) => {
|
||||
const [resource, action] = permission.split(':');
|
||||
return checkPermission(resource, action);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
jwtAuth,
|
||||
checkPermission,
|
||||
optionalAuth,
|
||||
authenticateToken,
|
||||
requirePermission
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
const User = require('../models/User');
|
||||
const Role = require('../models/Role');
|
||||
|
||||
/**
|
||||
* 固定Token认证中间件
|
||||
* 支持JWT token和固定token两种认证方式
|
||||
*/
|
||||
const fixedTokenAuth = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
status: 'error',
|
||||
message: '未提供认证token'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否为Bearer token格式
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// 首先尝试固定token验证
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
fixed_token: token,
|
||||
status: 'active'
|
||||
},
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role',
|
||||
attributes: ['id', 'name', 'permissions']
|
||||
}]
|
||||
});
|
||||
|
||||
if (user) {
|
||||
// 固定token验证成功
|
||||
req.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role_id: user.role_id,
|
||||
role: user.role,
|
||||
permissions: user.role ? user.role.permissions : []
|
||||
};
|
||||
return next();
|
||||
}
|
||||
|
||||
// 如果固定token验证失败,尝试JWT验证
|
||||
const jwt = require('jsonwebtoken');
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
if (decoded.type !== 'access') {
|
||||
return res.status(401).json({
|
||||
status: 'error',
|
||||
message: 'Token类型错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证用户是否存在且状态正常
|
||||
const jwtUser = await User.findOne({
|
||||
where: {
|
||||
id: decoded.userId,
|
||||
status: 'active'
|
||||
},
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role',
|
||||
attributes: ['id', 'name', 'permissions']
|
||||
}]
|
||||
});
|
||||
|
||||
if (!jwtUser) {
|
||||
return res.status(401).json({
|
||||
status: 'error',
|
||||
message: '用户不存在或已被禁用'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = {
|
||||
id: jwtUser.id,
|
||||
username: jwtUser.username,
|
||||
role_id: jwtUser.role_id,
|
||||
role: jwtUser.role,
|
||||
permissions: decoded.permissions || (jwtUser.role ? jwtUser.role.permissions : [])
|
||||
};
|
||||
|
||||
return next();
|
||||
} catch (jwtError) {
|
||||
if (jwtError.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({
|
||||
status: 'error',
|
||||
code: 'TOKEN_EXPIRED',
|
||||
message: 'Token已过期'
|
||||
});
|
||||
} else if (jwtError.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json({
|
||||
status: 'error',
|
||||
message: 'Token无效'
|
||||
});
|
||||
}
|
||||
throw jwtError;
|
||||
}
|
||||
} else {
|
||||
return res.status(401).json({
|
||||
status: 'error',
|
||||
message: 'Token格式错误,请使用Bearer格式'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token验证错误:', error);
|
||||
return res.status(500).json({
|
||||
status: 'error',
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = fixedTokenAuth;
|
||||
@@ -1,302 +0,0 @@
|
||||
const { OperationLog } = require('../models');
|
||||
|
||||
/**
|
||||
* 操作日志记录中间件
|
||||
* 自动记录用户的操作行为
|
||||
*/
|
||||
class OperationLogger {
|
||||
/**
|
||||
* 记录操作日志的中间件
|
||||
*/
|
||||
static logOperation(options = {}) {
|
||||
return async (req, res, next) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 保存原始的res.json方法
|
||||
const originalJson = res.json;
|
||||
|
||||
// 重写res.json方法以捕获响应数据
|
||||
res.json = function(data) {
|
||||
const endTime = Date.now();
|
||||
const executionTime = endTime - startTime;
|
||||
|
||||
// 异步记录操作日志,不阻塞响应
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await OperationLogger.recordLog(req, res, data, executionTime, options);
|
||||
} catch (error) {
|
||||
console.error('记录操作日志失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// 调用原始的json方法
|
||||
return originalJson.call(this, data);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录操作日志
|
||||
*/
|
||||
static async recordLog(req, res, responseData, executionTime, options) {
|
||||
try {
|
||||
// 如果用户未登录,不记录日志
|
||||
if (!req.user || !req.user.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取操作类型
|
||||
const operationType = OperationLogger.getOperationType(req.method, req.url, options);
|
||||
|
||||
// 获取操作模块
|
||||
const operationModule = OperationLogger.getOperationModule(req.url, options);
|
||||
|
||||
// 获取操作内容
|
||||
const operationContent = OperationLogger.getOperationContent(req, operationType, operationModule, options);
|
||||
|
||||
// 获取操作目标
|
||||
const operationTarget = OperationLogger.getOperationTarget(req, responseData, options);
|
||||
|
||||
// 获取操作状态
|
||||
const status = OperationLogger.getOperationStatus(res.statusCode, responseData);
|
||||
|
||||
// 获取错误信息
|
||||
const errorMessage = OperationLogger.getErrorMessage(responseData, status);
|
||||
|
||||
// 记录操作日志
|
||||
await OperationLog.logOperation({
|
||||
user_id: req.user.id,
|
||||
operation_type: operationType,
|
||||
operation_module: operationModule,
|
||||
operation_content: operationContent,
|
||||
operation_target: operationTarget,
|
||||
request_method: req.method,
|
||||
request_url: req.originalUrl || req.url,
|
||||
request_params: {
|
||||
query: req.query,
|
||||
body: OperationLogger.sanitizeRequestBody(req.body),
|
||||
params: req.params
|
||||
},
|
||||
response_status: res.statusCode,
|
||||
response_data: OperationLogger.sanitizeResponseData(responseData),
|
||||
ip_address: OperationLogger.getClientIP(req),
|
||||
user_agent: req.get('User-Agent') || '',
|
||||
execution_time: executionTime,
|
||||
status: status,
|
||||
error_message: errorMessage
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('记录操作日志时发生错误:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作类型
|
||||
*/
|
||||
static getOperationType(method, url, options) {
|
||||
if (options.operation_type) {
|
||||
return options.operation_type;
|
||||
}
|
||||
|
||||
// 根据URL和HTTP方法推断操作类型
|
||||
if (url.includes('/login')) return 'login';
|
||||
if (url.includes('/logout')) return 'logout';
|
||||
if (url.includes('/export')) return 'export';
|
||||
if (url.includes('/import')) return 'import';
|
||||
if (url.includes('/approve')) return 'approve';
|
||||
if (url.includes('/reject')) return 'reject';
|
||||
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
return 'view';
|
||||
case 'POST':
|
||||
return 'create';
|
||||
case 'PUT':
|
||||
case 'PATCH':
|
||||
return 'update';
|
||||
case 'DELETE':
|
||||
return 'delete';
|
||||
default:
|
||||
return 'other';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作模块
|
||||
*/
|
||||
static getOperationModule(url, options) {
|
||||
if (options.operation_module) {
|
||||
return options.operation_module;
|
||||
}
|
||||
|
||||
// 从URL中提取模块名
|
||||
const pathSegments = url.split('/').filter(segment => segment && segment !== 'api');
|
||||
if (pathSegments.length > 0) {
|
||||
return pathSegments[0].replace(/-/g, '_');
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作内容
|
||||
*/
|
||||
static getOperationContent(req, operationType, operationModule, options) {
|
||||
if (options.operation_content) {
|
||||
return options.operation_content;
|
||||
}
|
||||
|
||||
const actionMap = {
|
||||
'login': '用户登录',
|
||||
'logout': '用户退出',
|
||||
'view': '查看',
|
||||
'create': '创建',
|
||||
'update': '更新',
|
||||
'delete': '删除',
|
||||
'export': '导出',
|
||||
'import': '导入',
|
||||
'approve': '审批通过',
|
||||
'reject': '审批拒绝'
|
||||
};
|
||||
|
||||
const moduleMap = {
|
||||
'users': '用户',
|
||||
'roles': '角色',
|
||||
'insurance': '保险',
|
||||
'policies': '保单',
|
||||
'claims': '理赔',
|
||||
'system': '系统',
|
||||
'operation_logs': '操作日志',
|
||||
'devices': '设备',
|
||||
'device_alerts': '设备告警'
|
||||
};
|
||||
|
||||
const action = actionMap[operationType] || operationType;
|
||||
const module = moduleMap[operationModule] || operationModule;
|
||||
|
||||
return `${action}${module}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作目标
|
||||
*/
|
||||
static getOperationTarget(req, responseData, options) {
|
||||
if (options.operation_target) {
|
||||
return options.operation_target;
|
||||
}
|
||||
|
||||
// 尝试从请求参数中获取ID
|
||||
if (req.params.id) {
|
||||
return `ID: ${req.params.id}`;
|
||||
}
|
||||
|
||||
// 尝试从响应数据中获取信息
|
||||
if (responseData && responseData.data) {
|
||||
if (responseData.data.id) {
|
||||
return `ID: ${responseData.data.id}`;
|
||||
}
|
||||
if (responseData.data.name) {
|
||||
return `名称: ${responseData.data.name}`;
|
||||
}
|
||||
if (responseData.data.username) {
|
||||
return `用户: ${responseData.data.username}`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作状态
|
||||
*/
|
||||
static getOperationStatus(statusCode, responseData) {
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
return 'success';
|
||||
} else if (statusCode >= 400 && statusCode < 500) {
|
||||
return 'failed';
|
||||
} else {
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误信息
|
||||
*/
|
||||
static getErrorMessage(responseData, status) {
|
||||
if (status === 'success') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (responseData && responseData.message) {
|
||||
return responseData.message;
|
||||
}
|
||||
|
||||
if (responseData && responseData.error) {
|
||||
return responseData.error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求体数据(移除敏感信息)
|
||||
*/
|
||||
static sanitizeRequestBody(body) {
|
||||
if (!body || typeof body !== 'object') {
|
||||
return body;
|
||||
}
|
||||
|
||||
const sanitized = { ...body };
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
|
||||
|
||||
sensitiveFields.forEach(field => {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '***';
|
||||
}
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理响应数据(移除敏感信息)
|
||||
*/
|
||||
static sanitizeResponseData(data) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 只保留基本的响应信息,不保存完整的响应数据
|
||||
return {
|
||||
status: data.status,
|
||||
message: data.message,
|
||||
code: data.code
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*/
|
||||
static getClientIP(req) {
|
||||
return req.ip ||
|
||||
req.connection.remoteAddress ||
|
||||
req.socket.remoteAddress ||
|
||||
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
|
||||
'127.0.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建特定操作的日志记录器
|
||||
*/
|
||||
static createLogger(operationType, operationModule, operationContent) {
|
||||
return OperationLogger.logOperation({
|
||||
operation_type: operationType,
|
||||
operation_module: operationModule,
|
||||
operation_content: operationContent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OperationLogger;
|
||||
@@ -1,177 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('supervision_tasks', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '监管任务ID'
|
||||
},
|
||||
applicationId: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '申请单号',
|
||||
field: 'application_id'
|
||||
},
|
||||
policyId: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '保单编号',
|
||||
field: 'policy_id'
|
||||
},
|
||||
productName: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '产品名称',
|
||||
field: 'product_name'
|
||||
},
|
||||
insurancePeriod: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '保险期间',
|
||||
field: 'insurance_period'
|
||||
},
|
||||
customerName: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '客户姓名',
|
||||
field: 'customer_name'
|
||||
},
|
||||
documentType: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
comment: '证件类型',
|
||||
field: 'document_type'
|
||||
},
|
||||
documentNumber: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '证件号码',
|
||||
field: 'document_number'
|
||||
},
|
||||
applicableAmount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
comment: '适用金额',
|
||||
field: 'applicable_amount'
|
||||
},
|
||||
supervisionDataCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: '监管生成数量',
|
||||
field: 'supervision_data_count'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'processing', 'completed', 'rejected'),
|
||||
defaultValue: 'pending',
|
||||
comment: '状态: pending-待处理, processing-处理中, completed-已完成, rejected-已拒绝'
|
||||
},
|
||||
taskType: {
|
||||
type: DataTypes.ENUM('new_application', 'task_guidance', 'batch_operation'),
|
||||
allowNull: false,
|
||||
comment: '任务类型: new_application-新增监管任务, task_guidance-任务导入, batch_operation-批量新增',
|
||||
field: 'task_type'
|
||||
},
|
||||
assignedTo: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '分配给用户ID',
|
||||
field: 'assigned_to',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
priority: {
|
||||
type: DataTypes.ENUM('low', 'medium', 'high', 'urgent'),
|
||||
defaultValue: 'medium',
|
||||
comment: '优先级'
|
||||
},
|
||||
dueDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '截止日期',
|
||||
field: 'due_date'
|
||||
},
|
||||
completedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '完成时间',
|
||||
field: 'completed_at'
|
||||
},
|
||||
remarks: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注'
|
||||
},
|
||||
createdBy: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '创建人ID',
|
||||
field: 'created_by',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'RESTRICT'
|
||||
},
|
||||
updatedBy: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '更新人ID',
|
||||
field: 'updated_by',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.NOW,
|
||||
field: 'created_at'
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.NOW,
|
||||
field: 'updated_at'
|
||||
}
|
||||
}, {
|
||||
comment: '监管任务表',
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci'
|
||||
});
|
||||
|
||||
// 添加索引
|
||||
await queryInterface.addIndex('supervision_tasks', ['application_id']);
|
||||
await queryInterface.addIndex('supervision_tasks', ['policy_id']);
|
||||
await queryInterface.addIndex('supervision_tasks', ['customer_name']);
|
||||
await queryInterface.addIndex('supervision_tasks', ['status']);
|
||||
await queryInterface.addIndex('supervision_tasks', ['task_type']);
|
||||
await queryInterface.addIndex('supervision_tasks', ['assigned_to']);
|
||||
await queryInterface.addIndex('supervision_tasks', ['created_by']);
|
||||
await queryInterface.addIndex('supervision_tasks', ['created_at']);
|
||||
|
||||
// 添加唯一索引
|
||||
await queryInterface.addIndex('supervision_tasks', ['application_id'], {
|
||||
unique: true,
|
||||
name: 'unique_application_id'
|
||||
});
|
||||
await queryInterface.addIndex('supervision_tasks', ['policy_id'], {
|
||||
unique: true,
|
||||
name: 'unique_policy_id'
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('supervision_tasks');
|
||||
}
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('users', 'fixed_token', {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: true,
|
||||
unique: true,
|
||||
comment: '用户固定token,用于API访问验证'
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('users', 'fixed_token');
|
||||
}
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('operation_logs', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
user_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
operation_type: {
|
||||
type: Sequelize.ENUM(
|
||||
'login', // 登录
|
||||
'logout', // 登出
|
||||
'create', // 创建
|
||||
'update', // 更新
|
||||
'delete', // 删除
|
||||
'view', // 查看
|
||||
'export', // 导出
|
||||
'import', // 导入
|
||||
'approve', // 审批
|
||||
'reject', // 拒绝
|
||||
'system_config', // 系统配置
|
||||
'user_manage', // 用户管理
|
||||
'role_manage', // 角色管理
|
||||
'other' // 其他
|
||||
),
|
||||
allowNull: false,
|
||||
comment: '操作类型'
|
||||
},
|
||||
operation_module: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '操作模块(如:用户管理、设备管理、预警管理等)'
|
||||
},
|
||||
operation_content: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false,
|
||||
comment: '操作内容描述'
|
||||
},
|
||||
operation_target: {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '操作目标(如:用户ID、设备ID等)'
|
||||
},
|
||||
request_method: {
|
||||
type: Sequelize.ENUM('GET', 'POST', 'PUT', 'DELETE', 'PATCH'),
|
||||
allowNull: true,
|
||||
comment: 'HTTP请求方法'
|
||||
},
|
||||
request_url: {
|
||||
type: Sequelize.STRING(500),
|
||||
allowNull: true,
|
||||
comment: '请求URL'
|
||||
},
|
||||
request_params: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
comment: '请求参数(JSON格式)'
|
||||
},
|
||||
response_status: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '响应状态码'
|
||||
},
|
||||
ip_address: {
|
||||
type: Sequelize.STRING(45),
|
||||
allowNull: true,
|
||||
comment: 'IP地址(支持IPv6)'
|
||||
},
|
||||
user_agent: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
comment: '用户代理信息'
|
||||
},
|
||||
execution_time: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '执行时间(毫秒)'
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.ENUM('success', 'failed', 'error'),
|
||||
defaultValue: 'success',
|
||||
comment: '操作状态'
|
||||
},
|
||||
error_message: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
comment: '错误信息'
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
|
||||
}
|
||||
}, {
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci',
|
||||
comment: '系统操作日志表'
|
||||
});
|
||||
|
||||
// 创建索引
|
||||
await queryInterface.addIndex('operation_logs', ['user_id'], {
|
||||
name: 'idx_operation_logs_user_id'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('operation_logs', ['operation_type'], {
|
||||
name: 'idx_operation_logs_operation_type'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('operation_logs', ['operation_module'], {
|
||||
name: 'idx_operation_logs_operation_module'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('operation_logs', ['created_at'], {
|
||||
name: 'idx_operation_logs_created_at'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('operation_logs', ['status'], {
|
||||
name: 'idx_operation_logs_status'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('operation_logs', ['ip_address'], {
|
||||
name: 'idx_operation_logs_ip_address'
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('operation_logs');
|
||||
}
|
||||
};
|
||||
@@ -1,181 +0,0 @@
|
||||
/**
|
||||
* 创建监管任务表的迁移文件
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('supervisory_tasks', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '主键ID'
|
||||
},
|
||||
applicationNumber: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
field: 'application_number',
|
||||
comment: '申请单号'
|
||||
},
|
||||
policyNumber: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
field: 'policy_number',
|
||||
comment: '保单编号'
|
||||
},
|
||||
productName: {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: false,
|
||||
field: 'product_name',
|
||||
comment: '产品名称'
|
||||
},
|
||||
insurancePeriod: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
field: 'insurance_period',
|
||||
comment: '保险周期'
|
||||
},
|
||||
customerName: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
field: 'customer_name',
|
||||
comment: '客户姓名'
|
||||
},
|
||||
idType: {
|
||||
type: Sequelize.ENUM('身份证', '护照', '军官证', '士兵证', '港澳台居民居住证', '其他'),
|
||||
allowNull: false,
|
||||
defaultValue: '身份证',
|
||||
field: 'id_type',
|
||||
comment: '证件类型'
|
||||
},
|
||||
idNumber: {
|
||||
type: Sequelize.STRING(30),
|
||||
allowNull: false,
|
||||
field: 'id_number',
|
||||
comment: '证件号码'
|
||||
},
|
||||
applicableSupplies: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
field: 'applicable_supplies',
|
||||
comment: '适用生资(JSON格式存储)'
|
||||
},
|
||||
supervisorySuppliesQuantity: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
field: 'supervisory_supplies_quantity',
|
||||
comment: '监管生资数量'
|
||||
},
|
||||
taskStatus: {
|
||||
type: Sequelize.ENUM('待处理', '处理中', '已完成', '已取消'),
|
||||
allowNull: false,
|
||||
defaultValue: '待处理',
|
||||
field: 'task_status',
|
||||
comment: '任务状态'
|
||||
},
|
||||
priority: {
|
||||
type: Sequelize.ENUM('低', '中', '高', '紧急'),
|
||||
allowNull: false,
|
||||
defaultValue: '中',
|
||||
comment: '任务优先级'
|
||||
},
|
||||
assignedTo: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'assigned_to',
|
||||
comment: '分配给(用户ID)',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
dueDate: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
field: 'due_date',
|
||||
comment: '截止日期'
|
||||
},
|
||||
completedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
field: 'completed_at',
|
||||
comment: '完成时间'
|
||||
},
|
||||
notes: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注信息'
|
||||
},
|
||||
createdBy: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'created_by',
|
||||
comment: '创建人ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
updatedBy: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'updated_by',
|
||||
comment: '更新人ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
field: 'created_at',
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
field: 'updated_at',
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
|
||||
}
|
||||
}, {
|
||||
comment: '监管任务表'
|
||||
});
|
||||
|
||||
// 创建索引
|
||||
await queryInterface.addIndex('supervisory_tasks', ['application_number'], {
|
||||
name: 'idx_application_number'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('supervisory_tasks', ['policy_number'], {
|
||||
name: 'idx_policy_number'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('supervisory_tasks', ['customer_name'], {
|
||||
name: 'idx_customer_name'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('supervisory_tasks', ['task_status'], {
|
||||
name: 'idx_task_status'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('supervisory_tasks', ['created_at'], {
|
||||
name: 'idx_created_at'
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('supervisory_tasks');
|
||||
}
|
||||
};
|
||||
@@ -1,190 +0,0 @@
|
||||
/**
|
||||
* 创建待安装任务表的迁移文件
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.createTable('installation_tasks', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
comment: '主键ID'
|
||||
},
|
||||
applicationNumber: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
field: 'application_number',
|
||||
comment: '申请单号'
|
||||
},
|
||||
policyNumber: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
field: 'policy_number',
|
||||
comment: '保单编号'
|
||||
},
|
||||
productName: {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: false,
|
||||
field: 'product_name',
|
||||
comment: '产品名称'
|
||||
},
|
||||
customerName: {
|
||||
type: Sequelize.STRING(50),
|
||||
allowNull: false,
|
||||
field: 'customer_name',
|
||||
comment: '客户姓名'
|
||||
},
|
||||
idType: {
|
||||
type: Sequelize.ENUM('身份证', '护照', '军官证', '士兵证', '港澳台居民居住证', '其他'),
|
||||
allowNull: false,
|
||||
defaultValue: '身份证',
|
||||
field: 'id_type',
|
||||
comment: '证件类型'
|
||||
},
|
||||
idNumber: {
|
||||
type: Sequelize.STRING(30),
|
||||
allowNull: false,
|
||||
field: 'id_number',
|
||||
comment: '证件号码'
|
||||
},
|
||||
livestockSupplyType: {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: false,
|
||||
field: 'livestock_supply_type',
|
||||
comment: '养殖生资种类'
|
||||
},
|
||||
pendingDevices: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
field: 'pending_devices',
|
||||
comment: '待安装设备(JSON格式存储)'
|
||||
},
|
||||
installationStatus: {
|
||||
type: Sequelize.ENUM('待安装', '安装中', '已安装', '安装失败', '已取消'),
|
||||
allowNull: false,
|
||||
defaultValue: '待安装',
|
||||
field: 'installation_status',
|
||||
comment: '安装状态'
|
||||
},
|
||||
taskGeneratedTime: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
field: 'task_generated_time',
|
||||
comment: '生成安装任务时间'
|
||||
},
|
||||
installationCompletedTime: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
field: 'installation_completed_time',
|
||||
comment: '安装完成生效时间'
|
||||
},
|
||||
priority: {
|
||||
type: Sequelize.ENUM('低', '中', '高', '紧急'),
|
||||
allowNull: false,
|
||||
defaultValue: '中',
|
||||
comment: '安装优先级'
|
||||
},
|
||||
assignedTechnician: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'assigned_technician',
|
||||
comment: '分配的技术员ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
installationAddress: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
field: 'installation_address',
|
||||
comment: '安装地址'
|
||||
},
|
||||
contactPhone: {
|
||||
type: Sequelize.STRING(20),
|
||||
allowNull: true,
|
||||
field: 'contact_phone',
|
||||
comment: '联系电话'
|
||||
},
|
||||
remarks: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注信息'
|
||||
},
|
||||
createdBy: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'created_by',
|
||||
comment: '创建人ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
updatedBy: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
field: 'updated_by',
|
||||
comment: '更新人ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
field: 'created_at',
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
field: 'updated_at',
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')
|
||||
}
|
||||
}, {
|
||||
comment: '待安装任务表'
|
||||
});
|
||||
|
||||
// 创建索引
|
||||
await queryInterface.addIndex('installation_tasks', ['application_number'], {
|
||||
name: 'idx_application_number'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('installation_tasks', ['policy_number'], {
|
||||
name: 'idx_policy_number'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('installation_tasks', ['customer_name'], {
|
||||
name: 'idx_customer_name'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('installation_tasks', ['installation_status'], {
|
||||
name: 'idx_installation_status'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('installation_tasks', ['task_generated_time'], {
|
||||
name: 'idx_task_generated_time'
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('installation_tasks', ['installation_completed_time'], {
|
||||
name: 'idx_installation_completed_time'
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('installation_tasks');
|
||||
}
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Claim = sequelize.define('Claim', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
comment: '理赔ID'
|
||||
},
|
||||
claim_no: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '理赔编号',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '理赔编号不能为空'
|
||||
}
|
||||
}
|
||||
},
|
||||
policy_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '关联的保单ID',
|
||||
references: {
|
||||
model: 'policies',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
customer_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '客户ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
claim_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
comment: '理赔金额',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '理赔金额不能小于0'
|
||||
}
|
||||
}
|
||||
},
|
||||
claim_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: '理赔发生日期'
|
||||
},
|
||||
incident_description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
comment: '事故描述',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '事故描述不能为空'
|
||||
}
|
||||
}
|
||||
},
|
||||
claim_status: {
|
||||
type: DataTypes.ENUM('pending', 'approved', 'rejected', 'processing', 'paid'),
|
||||
allowNull: false,
|
||||
defaultValue: 'pending',
|
||||
comment: '理赔状态:pending-待审核,approved-已批准,rejected-已拒绝,processing-处理中,paid-已支付'
|
||||
},
|
||||
review_notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '审核备注'
|
||||
},
|
||||
reviewer_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '审核人ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
review_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '审核日期'
|
||||
},
|
||||
payment_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '支付日期'
|
||||
},
|
||||
supporting_documents: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
comment: '支持文件(JSON数组,包含文件URL和描述)'
|
||||
},
|
||||
created_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '创建人ID'
|
||||
},
|
||||
updated_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '更新人ID'
|
||||
}
|
||||
}, {
|
||||
tableName: 'claims',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_claim_no',
|
||||
fields: ['claim_no'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
name: 'idx_claim_policy',
|
||||
fields: ['policy_id']
|
||||
},
|
||||
{
|
||||
name: 'idx_claim_customer',
|
||||
fields: ['customer_id']
|
||||
},
|
||||
{
|
||||
name: 'idx_claim_status',
|
||||
fields: ['claim_status']
|
||||
},
|
||||
{
|
||||
name: 'idx_claim_date',
|
||||
fields: ['claim_date']
|
||||
}
|
||||
],
|
||||
comment: '理赔表'
|
||||
});
|
||||
|
||||
module.exports = Claim;
|
||||
@@ -1,86 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
/**
|
||||
* 设备模型
|
||||
* 用于管理保险相关的设备信息
|
||||
*/
|
||||
const Device = sequelize.define('Device', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '设备ID'
|
||||
},
|
||||
device_number: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '设备编号'
|
||||
},
|
||||
device_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '设备名称'
|
||||
},
|
||||
device_type: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '设备类型'
|
||||
},
|
||||
device_model: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: '设备型号'
|
||||
},
|
||||
manufacturer: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: '制造商'
|
||||
},
|
||||
installation_location: {
|
||||
type: DataTypes.STRING(200),
|
||||
comment: '安装位置'
|
||||
},
|
||||
installation_date: {
|
||||
type: DataTypes.DATE,
|
||||
comment: '安装日期'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('normal', 'warning', 'error', 'offline'),
|
||||
defaultValue: 'normal',
|
||||
comment: '设备状态:normal-正常,warning-警告,error-故障,offline-离线'
|
||||
},
|
||||
farm_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: '所属养殖场ID'
|
||||
},
|
||||
barn_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: '所属栏舍ID'
|
||||
},
|
||||
created_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: '创建人ID'
|
||||
},
|
||||
updated_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: '更新人ID'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '创建时间'
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '更新时间'
|
||||
}
|
||||
}, {
|
||||
tableName: 'devices',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
comment: '设备信息表'
|
||||
});
|
||||
|
||||
module.exports = Device;
|
||||
@@ -1,217 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const LivestockPolicy = sequelize.define('LivestockPolicy', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
comment: '生资保单ID'
|
||||
},
|
||||
policy_no: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '保单编号',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '保单编号不能为空'
|
||||
}
|
||||
}
|
||||
},
|
||||
livestock_type_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '牲畜类型ID',
|
||||
references: {
|
||||
model: 'livestock_types',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
policyholder_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '投保人姓名',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '投保人姓名不能为空'
|
||||
}
|
||||
}
|
||||
},
|
||||
policyholder_id_card: {
|
||||
type: DataTypes.STRING(18),
|
||||
allowNull: false,
|
||||
comment: '投保人身份证号',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '投保人身份证号不能为空'
|
||||
}
|
||||
}
|
||||
},
|
||||
policyholder_phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
comment: '投保人手机号',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '投保人手机号不能为空'
|
||||
}
|
||||
}
|
||||
},
|
||||
policyholder_address: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: false,
|
||||
comment: '投保人地址',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '投保人地址不能为空'
|
||||
}
|
||||
}
|
||||
},
|
||||
farm_name: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: true,
|
||||
comment: '养殖场名称'
|
||||
},
|
||||
farm_address: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: true,
|
||||
comment: '养殖场地址'
|
||||
},
|
||||
farm_license: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '养殖场许可证号'
|
||||
},
|
||||
livestock_count: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '投保牲畜数量',
|
||||
validate: {
|
||||
min: {
|
||||
args: [1],
|
||||
msg: '投保牲畜数量不能小于1'
|
||||
}
|
||||
}
|
||||
},
|
||||
unit_value: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
comment: '单头价值',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '单头价值不能小于0'
|
||||
}
|
||||
}
|
||||
},
|
||||
total_value: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
comment: '总保额',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '总保额不能小于0'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
premium_amount: {
|
||||
type: DataTypes.DECIMAL(15, 2),
|
||||
allowNull: false,
|
||||
comment: '保费金额',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '保费金额不能小于0'
|
||||
}
|
||||
}
|
||||
},
|
||||
start_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: '保险开始日期'
|
||||
},
|
||||
end_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: '保险结束日期'
|
||||
},
|
||||
policy_status: {
|
||||
type: DataTypes.ENUM('active', 'expired', 'cancelled', 'suspended'),
|
||||
allowNull: false,
|
||||
defaultValue: 'active',
|
||||
comment: '保单状态:active-有效,expired-已过期,cancelled-已取消,suspended-已暂停'
|
||||
},
|
||||
payment_status: {
|
||||
type: DataTypes.ENUM('paid', 'unpaid', 'partial'),
|
||||
allowNull: false,
|
||||
defaultValue: 'unpaid',
|
||||
comment: '支付状态:paid-已支付,unpaid-未支付,partial-部分支付'
|
||||
},
|
||||
payment_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '支付日期'
|
||||
},
|
||||
policy_document_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: true,
|
||||
comment: '保单文档URL'
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '备注信息'
|
||||
},
|
||||
created_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '创建人ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
updated_by: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '更新人ID',
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
tableName: 'livestock_policies',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_livestock_policy_no',
|
||||
fields: ['policy_no'],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
name: 'idx_livestock_policy_policyholder',
|
||||
fields: ['policyholder_name']
|
||||
},
|
||||
{
|
||||
name: 'idx_livestock_policy_status',
|
||||
fields: ['policy_status']
|
||||
},
|
||||
{
|
||||
name: 'idx_livestock_policy_payment_status',
|
||||
fields: ['payment_status']
|
||||
},
|
||||
{
|
||||
name: 'idx_livestock_policy_dates',
|
||||
fields: ['start_date', 'end_date']
|
||||
}
|
||||
],
|
||||
comment: '生资保单表'
|
||||
});
|
||||
|
||||
module.exports = LivestockPolicy;
|
||||
@@ -1,101 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const LivestockType = sequelize.define('LivestockType', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
comment: '牲畜类型ID'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '牲畜类型名称',
|
||||
validate: {
|
||||
notEmpty: {
|
||||
msg: '牲畜类型名称不能为空'
|
||||
}
|
||||
}
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '牲畜类型代码'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '牲畜类型描述'
|
||||
},
|
||||
unit_price_min: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
defaultValue: 0.00,
|
||||
comment: '单头最低价值',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '单头最低价值不能小于0'
|
||||
}
|
||||
}
|
||||
},
|
||||
unit_price_max: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false,
|
||||
defaultValue: 50000.00,
|
||||
comment: '单头最高价值',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '单头最高价值不能小于0'
|
||||
}
|
||||
}
|
||||
},
|
||||
premium_rate: {
|
||||
type: DataTypes.DECIMAL(5, 4),
|
||||
allowNull: false,
|
||||
defaultValue: 0.0050,
|
||||
comment: '保险费率',
|
||||
validate: {
|
||||
min: {
|
||||
args: [0],
|
||||
msg: '保费费率不能小于0'
|
||||
},
|
||||
max: {
|
||||
args: [1],
|
||||
msg: '保费费率不能大于1'
|
||||
}
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
allowNull: false,
|
||||
defaultValue: 'active',
|
||||
comment: '状态'
|
||||
}
|
||||
}, {
|
||||
tableName: 'livestock_types',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at',
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_livestock_type_name',
|
||||
fields: ['name']
|
||||
},
|
||||
{
|
||||
name: 'idx_livestock_type_code',
|
||||
fields: ['code']
|
||||
},
|
||||
{
|
||||
name: 'idx_livestock_type_status',
|
||||
fields: ['status']
|
||||
}
|
||||
],
|
||||
comment: '牲畜类型表'
|
||||
});
|
||||
|
||||
module.exports = LivestockType;
|
||||
@@ -1,85 +0,0 @@
|
||||
const { sequelize } = require('../config/database');
|
||||
const { DataTypes } = require('sequelize');
|
||||
|
||||
const Menu = sequelize.define('Menu', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
validate: {
|
||||
len: [2, 50]
|
||||
}
|
||||
},
|
||||
key: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
len: [2, 50]
|
||||
}
|
||||
},
|
||||
path: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
validate: {
|
||||
len: [1, 100]
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true
|
||||
},
|
||||
parent_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'menus',
|
||||
key: 'id'
|
||||
},
|
||||
defaultValue: null
|
||||
},
|
||||
component: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true
|
||||
},
|
||||
order: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
defaultValue: 'active'
|
||||
},
|
||||
show: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'menus',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['key'] },
|
||||
{ fields: ['parent_id'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['order'] }
|
||||
]
|
||||
});
|
||||
|
||||
// 设置自关联
|
||||
Menu.hasMany(Menu, {
|
||||
as: 'children',
|
||||
foreignKey: 'parent_id'
|
||||
});
|
||||
|
||||
Menu.belongsTo(Menu, {
|
||||
as: 'parent',
|
||||
foreignKey: 'parent_id'
|
||||
});
|
||||
|
||||
module.exports = Menu;
|
||||
@@ -1,42 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const MenuPermission = sequelize.define('MenuPermission', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
comment: '关联ID'
|
||||
},
|
||||
menu_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '菜单ID'
|
||||
},
|
||||
permission_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '权限ID'
|
||||
},
|
||||
required: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否必需权限'
|
||||
}
|
||||
}, {
|
||||
tableName: 'menu_permissions',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['menu_id', 'permission_id'],
|
||||
unique: true,
|
||||
name: 'uk_menu_permission'
|
||||
},
|
||||
{ fields: ['menu_id'] },
|
||||
{ fields: ['permission_id'] }
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = MenuPermission;
|
||||
@@ -1,270 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const OperationLog = sequelize.define('OperationLog', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
},
|
||||
comment: '操作用户ID'
|
||||
},
|
||||
operation_type: {
|
||||
type: DataTypes.ENUM(
|
||||
'login', // 登录
|
||||
'logout', // 登出
|
||||
'create', // 创建
|
||||
'update', // 更新
|
||||
'delete', // 删除
|
||||
'view', // 查看
|
||||
'export', // 导出
|
||||
'import', // 导入
|
||||
'approve', // 审批
|
||||
'reject', // 拒绝
|
||||
'system_config', // 系统配置
|
||||
'user_manage', // 用户管理
|
||||
'role_manage', // 角色管理
|
||||
'other' // 其他
|
||||
),
|
||||
allowNull: false,
|
||||
comment: '操作类型'
|
||||
},
|
||||
operation_module: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '操作模块(如:用户管理、设备管理、预警管理等)'
|
||||
},
|
||||
operation_content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
comment: '操作内容描述'
|
||||
},
|
||||
operation_target: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '操作目标(如:用户ID、设备ID等)'
|
||||
},
|
||||
request_method: {
|
||||
type: DataTypes.ENUM('GET', 'POST', 'PUT', 'DELETE', 'PATCH'),
|
||||
allowNull: true,
|
||||
comment: 'HTTP请求方法'
|
||||
},
|
||||
request_url: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: true,
|
||||
comment: '请求URL'
|
||||
},
|
||||
request_params: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '请求参数(JSON格式)',
|
||||
get() {
|
||||
const value = this.getDataValue('request_params');
|
||||
return value ? JSON.parse(value) : null;
|
||||
},
|
||||
set(value) {
|
||||
this.setDataValue('request_params', value ? JSON.stringify(value) : null);
|
||||
}
|
||||
},
|
||||
response_status: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '响应状态码'
|
||||
},
|
||||
ip_address: {
|
||||
type: DataTypes.STRING(45),
|
||||
allowNull: true,
|
||||
comment: 'IP地址(支持IPv6)'
|
||||
},
|
||||
user_agent: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '用户代理信息'
|
||||
},
|
||||
execution_time: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '执行时间(毫秒)'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('success', 'failed', 'error'),
|
||||
defaultValue: 'success',
|
||||
comment: '操作状态'
|
||||
},
|
||||
error_message: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '错误信息'
|
||||
}
|
||||
}, {
|
||||
tableName: 'operation_logs',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['user_id'] },
|
||||
{ fields: ['operation_type'] },
|
||||
{ fields: ['operation_module'] },
|
||||
{ fields: ['created_at'] },
|
||||
{ fields: ['status'] },
|
||||
{ fields: ['ip_address'] }
|
||||
]
|
||||
});
|
||||
|
||||
// 定义关联关系
|
||||
OperationLog.associate = function(models) {
|
||||
// 操作日志属于用户
|
||||
OperationLog.belongsTo(models.User, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'user',
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
};
|
||||
|
||||
// 静态方法:记录操作日志
|
||||
OperationLog.logOperation = async function(logData) {
|
||||
try {
|
||||
const log = await this.create({
|
||||
user_id: logData.userId,
|
||||
operation_type: logData.operationType,
|
||||
operation_module: logData.operationModule,
|
||||
operation_content: logData.operationContent,
|
||||
operation_target: logData.operationTarget,
|
||||
request_method: logData.requestMethod,
|
||||
request_url: logData.requestUrl,
|
||||
request_params: logData.requestParams,
|
||||
response_status: logData.responseStatus,
|
||||
ip_address: logData.ipAddress,
|
||||
user_agent: logData.userAgent,
|
||||
execution_time: logData.executionTime,
|
||||
status: logData.status || 'success',
|
||||
error_message: logData.errorMessage
|
||||
});
|
||||
return log;
|
||||
} catch (error) {
|
||||
console.error('记录操作日志失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 静态方法:获取操作日志列表
|
||||
OperationLog.getLogsList = async function(options = {}) {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
userId,
|
||||
operationType,
|
||||
operationModule,
|
||||
status,
|
||||
startDate,
|
||||
endDate,
|
||||
keyword
|
||||
} = options;
|
||||
|
||||
const where = {};
|
||||
|
||||
// 构建查询条件
|
||||
if (userId) where.user_id = userId;
|
||||
if (operationType) where.operation_type = operationType;
|
||||
if (operationModule) where.operation_module = operationModule;
|
||||
if (status) where.status = status;
|
||||
|
||||
// 时间范围查询
|
||||
if (startDate || endDate) {
|
||||
where.created_at = {};
|
||||
if (startDate) where.created_at[sequelize.Op.gte] = new Date(startDate);
|
||||
if (endDate) where.created_at[sequelize.Op.lte] = new Date(endDate);
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
where[sequelize.Op.or] = [
|
||||
{ operation_content: { [sequelize.Op.like]: `%${keyword}%` } },
|
||||
{ operation_target: { [sequelize.Op.like]: `%${keyword}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const result = await this.findAndCountAll({
|
||||
where,
|
||||
include: [{
|
||||
model: sequelize.models.User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name']
|
||||
}],
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset)
|
||||
});
|
||||
|
||||
return {
|
||||
logs: result.rows,
|
||||
total: result.count,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
totalPages: Math.ceil(result.count / limit)
|
||||
};
|
||||
};
|
||||
|
||||
// 静态方法:获取操作统计
|
||||
OperationLog.getOperationStats = async function(options = {}) {
|
||||
const { startDate, endDate, userId } = options;
|
||||
|
||||
const where = {};
|
||||
if (userId) where.user_id = userId;
|
||||
|
||||
if (startDate || endDate) {
|
||||
where.created_at = {};
|
||||
if (startDate) where.created_at[sequelize.Op.gte] = new Date(startDate);
|
||||
if (endDate) where.created_at[sequelize.Op.lte] = new Date(endDate);
|
||||
}
|
||||
|
||||
// 按操作类型统计
|
||||
const typeStats = await this.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'operation_type',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['operation_type'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 按操作模块统计
|
||||
const moduleStats = await this.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'operation_module',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['operation_module'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 按状态统计
|
||||
const statusStats = await this.findAll({
|
||||
where,
|
||||
attributes: [
|
||||
'status',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['status'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return {
|
||||
typeStats,
|
||||
moduleStats,
|
||||
statusStats
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = OperationLog;
|
||||
@@ -1,93 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
const Permission = sequelize.define('Permission', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
comment: '权限ID'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
comment: '权限名称',
|
||||
validate: {
|
||||
len: [2, 100]
|
||||
}
|
||||
},
|
||||
code: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '权限代码',
|
||||
validate: {
|
||||
len: [2, 100],
|
||||
is: /^[a-zA-Z0-9_:]+$/ // 只允许字母、数字、下划线和冒号
|
||||
}
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '权限描述'
|
||||
},
|
||||
module: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '所属模块',
|
||||
validate: {
|
||||
len: [2, 50]
|
||||
}
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('menu', 'operation'),
|
||||
allowNull: false,
|
||||
defaultValue: 'operation',
|
||||
comment: '权限类型:menu-菜单权限,operation-操作权限'
|
||||
},
|
||||
parent_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
comment: '父权限ID'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
allowNull: false,
|
||||
defaultValue: 'active',
|
||||
comment: '状态'
|
||||
},
|
||||
sort_order: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '排序'
|
||||
}
|
||||
}, {
|
||||
tableName: 'permissions',
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
indexes: [
|
||||
{ fields: ['code'], unique: true },
|
||||
{ fields: ['module'] },
|
||||
{ fields: ['type'] },
|
||||
{ fields: ['parent_id'] },
|
||||
{ fields: ['status'] }
|
||||
]
|
||||
});
|
||||
|
||||
// 定义自关联关系
|
||||
Permission.hasMany(Permission, {
|
||||
as: 'children',
|
||||
foreignKey: 'parent_id',
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
|
||||
Permission.belongsTo(Permission, {
|
||||
as: 'parent',
|
||||
foreignKey: 'parent_id',
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
});
|
||||
|
||||
module.exports = Permission;
|
||||
@@ -78,6 +78,31 @@ const Policy = sequelize.define('Policy', {
|
||||
allowNull: false,
|
||||
comment: '保险结束日期'
|
||||
},
|
||||
policyholder_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '投保人姓名'
|
||||
},
|
||||
insured_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '被保险人姓名'
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true,
|
||||
comment: '联系电话'
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '电子邮箱'
|
||||
},
|
||||
address: {
|
||||
type: DataTypes.STRING(500),
|
||||
allowNull: true,
|
||||
comment: '联系地址'
|
||||
},
|
||||
policy_status: {
|
||||
type: DataTypes.ENUM('active', 'expired', 'cancelled', 'suspended'),
|
||||
allowNull: false,
|
||||
|
||||
1541
insurance_backend/package-lock.json
generated
1541
insurance_backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"name": "insurance_backend",
|
||||
"version": "1.0.0",
|
||||
"description": "保险端口后端服务",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
"start": "node src/app.js",
|
||||
"dev": "nodemon src/app.js",
|
||||
"test": "jest",
|
||||
"migrate": "npx sequelize-cli db:migrate",
|
||||
"seed": "npx sequelize-cli db:seed:all",
|
||||
"migrate:undo": "npx sequelize-cli db:migrate:undo",
|
||||
"seed:undo": "npx sequelize-cli db:seed:undo:all"
|
||||
},
|
||||
"keywords": [
|
||||
"insurance",
|
||||
"backend",
|
||||
"nodejs",
|
||||
"express"
|
||||
],
|
||||
"author": "Insurance Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"bcrypt": "^5.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^8.1.0",
|
||||
"helmet": "^8.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"redis": "^4.5.0",
|
||||
"sanitize-html": "^2.8.1",
|
||||
"sequelize": "^6.29.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"validator": "^13.9.0",
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"jest": "^29.5.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"sequelize-cli": "^6.6.0",
|
||||
"supertest": "^6.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "16.20.2",
|
||||
"npm": ">=8.0.0"
|
||||
}
|
||||
}
|
||||
@@ -33,4 +33,9 @@ router.patch('/:id/status', jwtAuth, checkPermission('insurance:policy', 'edit')
|
||||
policyController.updatePolicyStatus
|
||||
);
|
||||
|
||||
// 删除保单
|
||||
router.delete('/:id', jwtAuth, checkPermission('insurance:policy', 'delete'),
|
||||
policyController.deletePolicy
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,75 +0,0 @@
|
||||
const { sequelize } = require('./config/database.js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
async function runMigration() {
|
||||
try {
|
||||
console.log('开始运行数据库迁移...');
|
||||
|
||||
// 测试数据库连接
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 获取所有迁移文件
|
||||
const migrationsPath = path.join(__dirname, 'migrations');
|
||||
const migrationFiles = fs.readdirSync(migrationsPath)
|
||||
.filter(file => file.endsWith('.js'))
|
||||
.sort();
|
||||
|
||||
console.log(`找到 ${migrationFiles.length} 个迁移文件`);
|
||||
|
||||
// 确保 SequelizeMeta 表存在
|
||||
await sequelize.query(`
|
||||
CREATE TABLE IF NOT EXISTS \`SequelizeMeta\` (
|
||||
\`name\` varchar(255) COLLATE utf8mb3_unicode_ci NOT NULL,
|
||||
PRIMARY KEY (\`name\`),
|
||||
UNIQUE KEY \`name\` (\`name\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
|
||||
`);
|
||||
|
||||
// 检查哪些迁移已经运行过
|
||||
const [executedMigrations] = await sequelize.query(
|
||||
'SELECT name FROM SequelizeMeta ORDER BY name'
|
||||
);
|
||||
const executedNames = executedMigrations.map(row => row.name);
|
||||
|
||||
// 运行未执行的迁移
|
||||
for (const file of migrationFiles) {
|
||||
if (!executedNames.includes(file)) {
|
||||
console.log(`正在运行迁移: ${file}`);
|
||||
|
||||
try {
|
||||
const migration = require(path.join(migrationsPath, file));
|
||||
await migration.up(sequelize.getQueryInterface(), sequelize.constructor);
|
||||
|
||||
// 记录迁移已执行
|
||||
await sequelize.query(
|
||||
'INSERT INTO SequelizeMeta (name) VALUES (?)',
|
||||
{ replacements: [file] }
|
||||
);
|
||||
|
||||
console.log(`✅ 迁移 ${file} 执行成功`);
|
||||
} catch (error) {
|
||||
console.error(`❌ 迁移 ${file} 执行失败:`, error);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
console.log(`⏭️ 迁移 ${file} 已执行,跳过`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 所有迁移执行完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 迁移执行失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 运行迁移
|
||||
runMigration().catch(error => {
|
||||
console.error('迁移过程中发生错误:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -19,7 +19,10 @@ const PORT = process.env.PORT || 3000;
|
||||
// 安全中间件
|
||||
app.use(helmet());
|
||||
app.use(cors({
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:3001',
|
||||
origin: [
|
||||
process.env.FRONTEND_URL || 'http://localhost:3001',
|
||||
'http://localhost:3002'
|
||||
],
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
const { sequelize } = require('./config/database');
|
||||
const jwt = require('jsonwebtoken');
|
||||
require('dotenv').config();
|
||||
|
||||
// 测试数据库连接
|
||||
async function testDatabaseConnection() {
|
||||
try {
|
||||
console.log('\n=== 测试数据库连接 ===');
|
||||
console.log('使用配置:');
|
||||
console.log(`- 主机: ${process.env.DB_HOST || '默认值'}`);
|
||||
console.log(`- 端口: ${process.env.DB_PORT || '默认值'}`);
|
||||
console.log(`- 数据库: ${process.env.DB_DATABASE || process.env.DB_NAME || '默认值'}`);
|
||||
console.log(`- 用户名: ${process.env.DB_USER || '默认值'}`);
|
||||
console.log(`- 密码: ${process.env.DB_PASSWORD ? '已设置 (不显示)' : '未设置'}`);
|
||||
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接失败:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试JWT配置
|
||||
function testJWTConfig() {
|
||||
try {
|
||||
console.log('\n=== 测试JWT配置 ===');
|
||||
console.log(`- JWT_SECRET: ${process.env.JWT_SECRET ? '已设置 (长度: ' + process.env.JWT_SECRET.length + ')' : '未设置'}`);
|
||||
console.log(`- JWT_EXPIRE: ${process.env.JWT_EXPIRE || '默认值'}`);
|
||||
|
||||
if (!process.env.JWT_SECRET) {
|
||||
console.error('❌ JWT_SECRET未设置!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试生成并验证令牌
|
||||
const testPayload = { test: 'data' };
|
||||
const token = jwt.sign(testPayload, process.env.JWT_SECRET, { expiresIn: '1h' });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
|
||||
console.log('✅ JWT配置有效!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ JWT配置错误:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 测试模型导入
|
||||
async function testModels() {
|
||||
try {
|
||||
console.log('\n=== 测试模型导入 ===');
|
||||
const { User, Role } = require('./models');
|
||||
console.log('✅ 用户模型导入成功');
|
||||
console.log('✅ 角色模型导入成功');
|
||||
|
||||
// 尝试查询用户表结构
|
||||
const userAttributes = User.rawAttributes;
|
||||
console.log(`✅ 用户表有 ${Object.keys(userAttributes).length} 个字段`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 模型导入错误:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
async function runTests() {
|
||||
console.log('\n开始测试认证相关配置...');
|
||||
|
||||
const dbResult = await testDatabaseConnection();
|
||||
const jwtResult = testJWTConfig();
|
||||
const modelsResult = await testModels();
|
||||
|
||||
console.log('\n=== 测试总结 ===');
|
||||
console.log(`数据库连接: ${dbResult ? '通过' : '失败'}`);
|
||||
console.log(`JWT配置: ${jwtResult ? '通过' : '失败'}`);
|
||||
console.log(`模型导入: ${modelsResult ? '通过' : '失败'}`);
|
||||
|
||||
if (dbResult && jwtResult && modelsResult) {
|
||||
console.log('✅ 所有测试通过!');
|
||||
} else {
|
||||
console.error('❌ 测试失败,请检查上述错误!');
|
||||
}
|
||||
|
||||
// 关闭数据库连接
|
||||
await sequelize.close();
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
runTests().catch(error => {
|
||||
console.error('测试过程中出现未捕获错误:', error);
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
const { sequelize, testConnection } = require('./config/database.js');
|
||||
|
||||
// 测试数据库连接
|
||||
async function runTest() {
|
||||
console.log('=== 数据库连接测试开始 ===');
|
||||
console.log('环境变量检查:');
|
||||
console.log(`- DB_HOST: ${process.env.DB_HOST}`);
|
||||
console.log(`- DB_PORT: ${process.env.DB_PORT}`);
|
||||
console.log(`- DB_DATABASE: ${process.env.DB_DATABASE}`);
|
||||
console.log(`- DB_USER: ${process.env.DB_USER}`);
|
||||
console.log(`- DB_PASSWORD: ${process.env.DB_PASSWORD ? '已设置' : '未设置'}`);
|
||||
|
||||
console.log('\n连接参数检查:');
|
||||
console.log(`- 实际使用的主机: ${sequelize.config.host}`);
|
||||
console.log(`- 实际使用的端口: ${sequelize.config.port}`);
|
||||
console.log(`- 实际使用的数据库: ${sequelize.config.database}`);
|
||||
console.log(`- 实际使用的用户名: ${sequelize.config.username}`);
|
||||
console.log(`- 实际使用的密码: ${sequelize.config.password ? '已设置' : '未设置'}`);
|
||||
|
||||
console.log('\n正在尝试连接数据库...');
|
||||
const success = await testConnection();
|
||||
|
||||
if (success) {
|
||||
console.log('✅ 测试成功!数据库连接已建立。');
|
||||
console.log('\n请尝试重新启动应用服务器。');
|
||||
} else {
|
||||
console.log('❌ 测试失败。请检查数据库配置和服务状态。');
|
||||
console.log('\n可能的解决方案:');
|
||||
console.log('1. 确认MySQL服务正在运行');
|
||||
console.log('2. 确认用户名和密码正确');
|
||||
console.log('3. 确认数据库已创建');
|
||||
console.log('4. 确认用户有足够的权限访问该数据库');
|
||||
}
|
||||
|
||||
console.log('=== 数据库连接测试结束 ===');
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
runTest().catch(error => {
|
||||
console.error('测试执行过程中发生错误:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
const express = require('express');
|
||||
|
||||
// 测试路由加载
|
||||
console.log('开始测试路由加载...');
|
||||
|
||||
try {
|
||||
// 测试设备路由
|
||||
const deviceRoutes = require('./routes/devices');
|
||||
console.log('✅ 设备路由加载成功');
|
||||
|
||||
// 测试设备控制器
|
||||
const deviceController = require('./controllers/deviceController');
|
||||
console.log('✅ 设备控制器加载成功');
|
||||
|
||||
// 测试模型
|
||||
const { Device, DeviceAlert } = require('./models');
|
||||
console.log('✅ 设备模型加载成功');
|
||||
|
||||
// 创建简单的Express应用测试
|
||||
const app = express();
|
||||
app.use('/api/devices', deviceRoutes);
|
||||
|
||||
console.log('✅ 路由注册成功');
|
||||
console.log('所有组件加载正常!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 路由加载失败:', error.message);
|
||||
console.error('错误详情:', error);
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
const http = require('http');
|
||||
|
||||
// 测试服务器健康检查接口
|
||||
function testHealthCheck() {
|
||||
return new Promise((resolve) => {
|
||||
http.get('http://localhost:3000/health', (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('✅ 健康检查接口测试成功!');
|
||||
console.log('响应状态码:', res.statusCode);
|
||||
console.log('响应数据:', JSON.parse(data));
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error('❌ 健康检查接口测试失败:', `状态码: ${res.statusCode}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}).on('error', (error) => {
|
||||
console.error('❌ 健康检查接口测试失败:', error.message);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 测试API文档接口
|
||||
function testApiDocs() {
|
||||
return new Promise((resolve) => {
|
||||
http.get('http://localhost:3000/api-docs', (res) => {
|
||||
if (res.statusCode === 200 || res.statusCode === 301 || res.statusCode === 302) {
|
||||
console.log('✅ API文档接口测试成功!');
|
||||
console.log('响应状态码:', res.statusCode);
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error('❌ API文档接口测试失败:', `状态码: ${res.statusCode}`);
|
||||
resolve(false);
|
||||
}
|
||||
}).on('error', (error) => {
|
||||
console.error('❌ API文档接口测试失败:', error.message);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 主测试函数
|
||||
async function runTests() {
|
||||
console.log('开始测试保险后端服务...\n');
|
||||
|
||||
const healthCheckResult = await testHealthCheck();
|
||||
console.log('');
|
||||
const apiDocsResult = await testApiDocs();
|
||||
|
||||
console.log('\n测试总结:');
|
||||
if (healthCheckResult && apiDocsResult) {
|
||||
console.log('✅ 所有测试通过! 服务器已成功启动并可访问基础接口。');
|
||||
console.log('注意: 数据库连接仍存在问题,但不影响基础接口的访问。');
|
||||
console.log('请在浏览器中访问以下地址:');
|
||||
console.log(' - 健康检查: http://localhost:3000/health');
|
||||
console.log(' - API文档: http://localhost:3000/api-docs');
|
||||
} else {
|
||||
console.log('❌ 部分测试失败,请检查服务器配置。');
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
runTests();
|
||||
@@ -1,44 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
async function testSupervisionTaskAPI() {
|
||||
const baseURL = 'http://localhost:3000/api/supervision-tasks';
|
||||
|
||||
try {
|
||||
// 1. 测试获取列表
|
||||
console.log('=== 测试获取监管任务列表 ===');
|
||||
const getResponse = await axios.get(baseURL);
|
||||
console.log('GET请求成功:', getResponse.data);
|
||||
|
||||
// 2. 测试创建任务
|
||||
console.log('\n=== 测试创建监管任务 ===');
|
||||
const taskData = {
|
||||
applicationNumber: "APP2025001",
|
||||
policyNumber: "POL2025001",
|
||||
productName: "农业保险产品",
|
||||
insurancePeriod: "2025-01-01至2025-12-31",
|
||||
customerName: "张三",
|
||||
idType: "身份证",
|
||||
idNumber: "110101199001011234",
|
||||
supervisorySuppliesQuantity: 100,
|
||||
taskStatus: "待处理",
|
||||
priority: "中",
|
||||
notes: "测试监管任务"
|
||||
};
|
||||
|
||||
const createResponse = await axios.post(baseURL, taskData);
|
||||
console.log('POST请求成功:', createResponse.data);
|
||||
|
||||
// 3. 测试获取详情
|
||||
if (createResponse.data.data && createResponse.data.data.id) {
|
||||
console.log('\n=== 测试获取任务详情 ===');
|
||||
const taskId = createResponse.data.data.id;
|
||||
const detailResponse = await axios.get(`${baseURL}/${taskId}`);
|
||||
console.log('GET详情请求成功:', detailResponse.data);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('API测试失败:', error.response ? error.response.data : error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testSupervisionTaskAPI();
|
||||
@@ -1,34 +0,0 @@
|
||||
// 简单的API测试脚本
|
||||
const axios = require('axios');
|
||||
|
||||
async function testAuthenticatedApi() {
|
||||
try {
|
||||
console.log('测试认证菜单API...');
|
||||
// 注意:这个测试需要有效的JWT token
|
||||
const response = await axios.get('http://localhost:3000/api/menus', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer YOUR_JWT_TOKEN_HERE'
|
||||
}
|
||||
});
|
||||
console.log('✅ 认证菜单API测试成功');
|
||||
console.log('返回数据:', response.data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ 认证菜单API测试失败:', error.message);
|
||||
console.log('提示:请确保服务器运行并提供有效的JWT token');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testAllApis() {
|
||||
console.log('开始API测试...');
|
||||
console.log('注意:所有API现在都需要认证,请确保提供有效的JWT token');
|
||||
const authApiResult = await testAuthenticatedApi();
|
||||
|
||||
console.log('\n测试总结:');
|
||||
console.log(`认证菜单API: ${authApiResult ? '通过' : '失败'}`);
|
||||
}
|
||||
|
||||
testAllApis().then(() => {
|
||||
console.log('\nAPI测试完成');
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
// 创建一个模拟浏览器的axios实例
|
||||
const browserAPI = axios.create({
|
||||
baseURL: 'http://localhost:3001',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
async function testBrowserBehavior() {
|
||||
console.log('=== 模拟浏览器行为测试 ===\n');
|
||||
|
||||
try {
|
||||
// 1. 模拟前端登录
|
||||
console.log('1. 模拟浏览器登录...');
|
||||
const loginResponse = await browserAPI.post('/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
console.log('登录响应状态:', loginResponse.status);
|
||||
console.log('登录响应数据:', JSON.stringify(loginResponse.data, null, 2));
|
||||
|
||||
if (!loginResponse.data || loginResponse.data.code !== 200) {
|
||||
console.log('❌ 登录失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = loginResponse.data.data.token;
|
||||
console.log('✅ 获取到Token:', token.substring(0, 50) + '...');
|
||||
|
||||
// 2. 设置Authorization header
|
||||
browserAPI.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
// 3. 模拟前端API调用
|
||||
console.log('\n2. 模拟浏览器API调用...');
|
||||
|
||||
try {
|
||||
const apiResponse = await browserAPI.get('/api/data-warehouse/overview');
|
||||
|
||||
console.log('✅ API调用成功!');
|
||||
console.log('状态码:', apiResponse.status);
|
||||
console.log('响应数据:', JSON.stringify(apiResponse.data, null, 2));
|
||||
|
||||
} catch (apiError) {
|
||||
console.log('❌ API调用失败:', apiError.response?.status, apiError.response?.statusText);
|
||||
console.log('错误详情:', apiError.response?.data);
|
||||
console.log('请求头:', apiError.config?.headers);
|
||||
|
||||
// 检查是否是权限问题
|
||||
if (apiError.response?.status === 403) {
|
||||
console.log('\n🔍 403错误分析:');
|
||||
console.log('- Token是否正确传递:', !!apiError.config?.headers?.Authorization);
|
||||
console.log('- Authorization头:', apiError.config?.headers?.Authorization?.substring(0, 50) + '...');
|
||||
|
||||
// 尝试验证token
|
||||
console.log('\n3. 验证Token有效性...');
|
||||
try {
|
||||
const profileResponse = await browserAPI.get('/api/auth/profile');
|
||||
console.log('✅ Token验证成功,用户信息:', profileResponse.data);
|
||||
} catch (profileError) {
|
||||
console.log('❌ Token验证失败:', profileError.response?.status, profileError.response?.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 测试其他需要权限的接口
|
||||
console.log('\n4. 测试其他权限接口...');
|
||||
const testAPIs = [
|
||||
'/api/insurance/applications',
|
||||
'/api/device-alerts/stats',
|
||||
'/api/system/stats'
|
||||
];
|
||||
|
||||
for (const apiPath of testAPIs) {
|
||||
try {
|
||||
const response = await browserAPI.get(apiPath);
|
||||
console.log(`✅ ${apiPath}: 成功 (${response.status})`);
|
||||
} catch (error) {
|
||||
console.log(`❌ ${apiPath}: 失败 (${error.response?.status}) - ${error.response?.data?.message || error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('❌ 测试失败:', error.response?.data || error.message);
|
||||
if (error.response) {
|
||||
console.log('错误状态:', error.response.status);
|
||||
console.log('错误数据:', error.response.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testBrowserBehavior();
|
||||
@@ -1,78 +0,0 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
// 确保正确加载.env文件
|
||||
require('dotenv').config();
|
||||
|
||||
console.log('环境变量加载情况:');
|
||||
console.log(`- DB_HOST: ${process.env.DB_HOST}`);
|
||||
console.log(`- DB_PORT: ${process.env.DB_PORT}`);
|
||||
console.log(`- DB_DATABASE: ${process.env.DB_DATABASE}`);
|
||||
console.log(`- DB_USER: ${process.env.DB_USER}`);
|
||||
console.log(`- NODE_ENV: ${process.env.NODE_ENV}`);
|
||||
|
||||
// 直接使用环境变量创建连接,不使用默认值
|
||||
const sequelize = new Sequelize({
|
||||
dialect: 'mysql',
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_DATABASE,
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
logging: false
|
||||
});
|
||||
|
||||
// 测试数据库连接
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
console.log('\n正在测试数据库连接...');
|
||||
console.log('使用的连接配置:');
|
||||
console.log(`- 主机: ${sequelize.config.host}`);
|
||||
console.log(`- 端口: ${sequelize.config.port}`);
|
||||
console.log(`- 数据库: ${sequelize.config.database}`);
|
||||
console.log(`- 用户名: ${sequelize.config.username}`);
|
||||
|
||||
// 测试连接
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功!');
|
||||
|
||||
// 测试查询
|
||||
try {
|
||||
const [results, metadata] = await sequelize.query('SELECT 1 AS test');
|
||||
console.log('✅ 数据库查询测试成功,结果:', results);
|
||||
|
||||
// 尝试查询数据库中的表
|
||||
const [tables, tableMeta] = await sequelize.query(
|
||||
"SHOW TABLES LIKE 'users'"
|
||||
);
|
||||
|
||||
if (tables.length > 0) {
|
||||
console.log('✅ 数据库中存在users表');
|
||||
|
||||
// 尝试查询用户表数据
|
||||
const [users, userMeta] = await sequelize.query('SELECT COUNT(*) AS user_count FROM users');
|
||||
console.log('✅ 用户表查询成功,用户数量:', users[0].user_count);
|
||||
} else {
|
||||
console.warn('⚠️ 数据库中不存在users表,请先运行数据库初始化脚本');
|
||||
}
|
||||
} catch (queryError) {
|
||||
console.error('⚠️ 数据库查询测试失败:', queryError.message);
|
||||
console.log('\n建议:');
|
||||
console.log('1. 确认数据库已创建并包含所需的表');
|
||||
console.log('2. 运行项目根目录下的数据库初始化脚本');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接失败:', error.message);
|
||||
console.log('\n可能的解决方案:');
|
||||
console.log('1. 确认MySQL服务正在运行');
|
||||
console.log('2. 确认.env文件中的用户名和密码正确');
|
||||
console.log('3. 确认.env文件中的数据库名称正确且已创建');
|
||||
console.log('4. 确认用户有足够的权限访问数据库');
|
||||
console.log('5. 检查网络连接是否正常');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
testConnection();
|
||||
@@ -1,52 +0,0 @@
|
||||
const { sequelize } = require('./config/database');
|
||||
|
||||
// 测试数据库连接
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
console.log('正在测试数据库连接...');
|
||||
console.log('连接配置:');
|
||||
console.log(`- 主机: ${sequelize.config.host}`);
|
||||
console.log(`- 端口: ${sequelize.config.port}`);
|
||||
console.log(`- 数据库: ${sequelize.config.database}`);
|
||||
console.log(`- 用户名: ${sequelize.config.username}`);
|
||||
|
||||
// 测试连接
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功!');
|
||||
|
||||
// 测试查询
|
||||
try {
|
||||
const [results, metadata] = await sequelize.query('SELECT 1 AS test');
|
||||
console.log('✅ 数据库查询测试成功,结果:', results);
|
||||
|
||||
// 尝试查询用户表
|
||||
try {
|
||||
const [users, userMeta] = await sequelize.query('SELECT COUNT(*) AS user_count FROM users');
|
||||
console.log('✅ 用户表查询成功,用户数量:', users[0].user_count);
|
||||
|
||||
// 尝试查询角色表
|
||||
const [roles, roleMeta] = await sequelize.query('SELECT COUNT(*) AS role_count FROM roles');
|
||||
console.log('✅ 角色表查询成功,角色数量:', roles[0].role_count);
|
||||
|
||||
} catch (tableError) {
|
||||
console.error('⚠️ 表查询失败,可能是表不存在:', tableError.message);
|
||||
}
|
||||
} catch (queryError) {
|
||||
console.error('⚠️ 数据库查询测试失败:', queryError.message);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库连接失败:', error.message);
|
||||
console.log('\n可能的解决方案:');
|
||||
console.log('1. 确认MySQL服务正在运行');
|
||||
console.log('2. 确认用户名和密码正确');
|
||||
console.log('3. 确认数据库存在');
|
||||
console.log('4. 确认用户有足够的权限访问数据库');
|
||||
console.log('5. 检查网络连接是否正常');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
testConnection();
|
||||
@@ -1,37 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
async function testServer() {
|
||||
console.log('开始测试服务器...');
|
||||
|
||||
try {
|
||||
// 测试健康检查
|
||||
console.log('\n1. 测试健康检查接口...');
|
||||
const healthResponse = await axios.get('http://localhost:3004/health');
|
||||
console.log('健康检查成功:', healthResponse.data);
|
||||
} catch (error) {
|
||||
console.log('健康检查失败:', error.response?.status, error.response?.data || error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
// 测试登录接口
|
||||
console.log('\n2. 测试登录接口...');
|
||||
const loginResponse = await axios.post('http://localhost:3004/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
console.log('登录成功:', loginResponse.data);
|
||||
} catch (error) {
|
||||
console.log('登录失败:', error.response?.status, error.response?.data || error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
// 测试不存在的接口
|
||||
console.log('\n3. 测试不存在的接口...');
|
||||
const notFoundResponse = await axios.get('http://localhost:3004/nonexistent');
|
||||
console.log('不存在接口响应:', notFoundResponse.data);
|
||||
} catch (error) {
|
||||
console.log('不存在接口错误:', error.response?.status, error.response?.data || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testServer();
|
||||
@@ -1,109 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
|
||||
// 测试固定令牌功能
|
||||
async function testFixedToken() {
|
||||
try {
|
||||
console.log('🧪 开始测试固定令牌功能...\n');
|
||||
|
||||
// 1. 登录获取JWT令牌
|
||||
console.log('1. 登录获取JWT令牌...');
|
||||
const loginResponse = await axios.post(`${BASE_URL}/api/auth/login`, {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
if (loginResponse.data.code !== 200) {
|
||||
console.error('❌ 登录失败:', loginResponse.data.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const jwtToken = loginResponse.data.data.accessToken;
|
||||
const userId = loginResponse.data.data.user.id;
|
||||
console.log('✅ 登录成功,用户ID:', userId);
|
||||
|
||||
// 2. 检查用户是否已有固定令牌
|
||||
console.log('\n2. 检查用户固定令牌状态...');
|
||||
const tokenInfoResponse = await axios.get(`${BASE_URL}/api/users/${userId}/fixed-token`, {
|
||||
headers: { Authorization: `Bearer ${jwtToken}` }
|
||||
});
|
||||
|
||||
console.log('✅ 令牌状态:', tokenInfoResponse.data.data);
|
||||
|
||||
// 3. 如果没有固定令牌,则生成一个
|
||||
let fixedToken;
|
||||
if (!tokenInfoResponse.data.data.hasToken) {
|
||||
console.log('\n3. 生成固定令牌...');
|
||||
const generateResponse = await axios.post(`${BASE_URL}/api/users/${userId}/fixed-token`, {}, {
|
||||
headers: { Authorization: `Bearer ${jwtToken}` }
|
||||
});
|
||||
|
||||
if (generateResponse.data.code === 200) {
|
||||
fixedToken = generateResponse.data.data.fixed_token;
|
||||
console.log('✅ 固定令牌生成成功');
|
||||
console.log('🔑 固定令牌:', fixedToken);
|
||||
} else {
|
||||
console.error('❌ 固定令牌生成失败:', generateResponse.data.message);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log('\n3. 用户已有固定令牌,重新生成...');
|
||||
const regenerateResponse = await axios.put(`${BASE_URL}/api/users/${userId}/fixed-token`, {}, {
|
||||
headers: { Authorization: `Bearer ${jwtToken}` }
|
||||
});
|
||||
|
||||
if (regenerateResponse.data.code === 200) {
|
||||
fixedToken = regenerateResponse.data.data.fixed_token;
|
||||
console.log('✅ 固定令牌重新生成成功');
|
||||
console.log('🔑 新固定令牌:', fixedToken);
|
||||
} else {
|
||||
console.error('❌ 固定令牌重新生成失败:', regenerateResponse.data.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 使用固定令牌访问API
|
||||
console.log('\n4. 使用固定令牌访问用户列表API...');
|
||||
const usersResponse = await axios.get(`${BASE_URL}/api/users`, {
|
||||
headers: { Authorization: `Bearer ${fixedToken}` }
|
||||
});
|
||||
|
||||
if (usersResponse.data.code === 200) {
|
||||
console.log('✅ 使用固定令牌访问API成功');
|
||||
console.log('📊 用户数量:', usersResponse.data.data.users.length);
|
||||
} else {
|
||||
console.error('❌ 使用固定令牌访问API失败:', usersResponse.data.message);
|
||||
}
|
||||
|
||||
// 5. 测试无效令牌
|
||||
console.log('\n5. 测试无效固定令牌...');
|
||||
try {
|
||||
await axios.get(`${BASE_URL}/api/users`, {
|
||||
headers: { Authorization: 'Bearer ft_invalid_token' }
|
||||
});
|
||||
console.error('❌ 无效令牌测试失败:应该返回401错误');
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 401) {
|
||||
console.log('✅ 无效令牌正确返回401错误');
|
||||
} else {
|
||||
console.error('❌ 无效令牌测试异常:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 再次检查令牌信息
|
||||
console.log('\n6. 再次检查令牌信息...');
|
||||
const finalTokenInfoResponse = await axios.get(`${BASE_URL}/api/users/${userId}/fixed-token`, {
|
||||
headers: { Authorization: `Bearer ${jwtToken}` }
|
||||
});
|
||||
console.log('✅ 最终令牌状态:', finalTokenInfoResponse.data.data);
|
||||
|
||||
console.log('\n🎉 固定令牌功能测试完成!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试过程中发生错误:', error.response?.data || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testFixedToken();
|
||||
@@ -1,35 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
async function testAPI() {
|
||||
try {
|
||||
// 先登录获取token
|
||||
const loginResponse = await axios.post('http://localhost:3000/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
const token = loginResponse.data.data.token;
|
||||
console.log('✅ 登录成功');
|
||||
|
||||
// 测试获取牲畜类型列表
|
||||
const response = await axios.get('http://localhost:3000/api/livestock-types', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ 获取牲畜类型列表成功');
|
||||
console.log('响应数据:', JSON.stringify(response.data, null, 2));
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 错误:', error.response?.data || error.message);
|
||||
if (error.response?.data) {
|
||||
console.error('详细错误:', JSON.stringify(error.response.data, null, 2));
|
||||
}
|
||||
if (error.stack) {
|
||||
console.error('错误堆栈:', error.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testAPI();
|
||||
@@ -1,21 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('开始测试登录...');
|
||||
|
||||
const response = await axios.post('http://localhost:3000/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
console.log('登录成功!');
|
||||
console.log('状态码:', response.status);
|
||||
console.log('响应数据:', JSON.stringify(response.data, null, 2));
|
||||
|
||||
} catch (error) {
|
||||
console.log('登录失败!');
|
||||
console.log('状态码:', error.response?.status);
|
||||
console.log('错误信息:', error.response?.data || error.message);
|
||||
}
|
||||
})();
|
||||
@@ -1,32 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
async function testMenuAPI() {
|
||||
try {
|
||||
// 1. 先登录获取token
|
||||
console.log('1. 登录获取token...');
|
||||
const loginResponse = await axios.post('http://localhost:3000/api/auth/login', {
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
});
|
||||
|
||||
const token = loginResponse.data.data.accessToken;
|
||||
console.log('登录成功,token:', token.substring(0, 50) + '...');
|
||||
|
||||
// 2. 测试菜单接口
|
||||
console.log('\n2. 测试菜单接口...');
|
||||
const menuResponse = await axios.get('http://localhost:3000/api/menus', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('菜单接口响应状态:', menuResponse.status);
|
||||
console.log('菜单接口响应数据:', JSON.stringify(menuResponse.data, null, 2));
|
||||
|
||||
} catch (error) {
|
||||
console.error('测试失败:', error.response?.data || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testMenuAPI();
|
||||
@@ -1,23 +0,0 @@
|
||||
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();
|
||||
@@ -1,19 +0,0 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
const hash = '$2b$12$2gMSr66wlftS./7f7U9JJeSZrpOPTQUFXLUANJ3a0IfWoiKPCuSDO';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('测试密码验证...');
|
||||
|
||||
const passwords = ['admin', 'admin123', '123456', 'password', 'Admin123'];
|
||||
|
||||
for (const pwd of passwords) {
|
||||
const result = await bcrypt.compare(pwd, hash);
|
||||
console.log(`密码 '${pwd}': ${result ? '正确' : '错误'}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
}
|
||||
})();
|
||||
@@ -1,75 +0,0 @@
|
||||
const axios = require('axios');
|
||||
|
||||
const BASE_URL = 'http://localhost:3000/api';
|
||||
const ADMIN_TOKEN = '5659725423f665a8bf5053b37e624ea86387f9113ae77ac75fc102012a349180';
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${ADMIN_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
async function testRolePermissionsAPI() {
|
||||
console.log('开始测试角色权限管理API...\n');
|
||||
|
||||
// 测试1: 获取所有权限
|
||||
try {
|
||||
console.log('1. 测试获取所有权限...');
|
||||
const response = await api.get('/role-permissions/permissions');
|
||||
console.log('✅ 获取所有权限成功');
|
||||
console.log('权限数量:', response.data.data?.length || 0);
|
||||
} catch (error) {
|
||||
console.log('❌ 获取所有权限失败:', error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// 测试2: 获取所有角色及其权限
|
||||
try {
|
||||
console.log('\n2. 测试获取所有角色及其权限...');
|
||||
const response = await api.get('/role-permissions/roles');
|
||||
console.log('✅ 获取所有角色及其权限成功');
|
||||
const roles = response.data.data?.roles || [];
|
||||
console.log('角色数量:', roles.length);
|
||||
if (roles && roles.length > 0) {
|
||||
roles.forEach(role => {
|
||||
console.log(` - ${role.name}: ${role.permissionCount || 0} 个权限`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ 获取所有角色及其权限失败:', error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// 测试3: 获取特定角色的详细权限
|
||||
try {
|
||||
console.log('\n3. 测试获取特定角色的详细权限...');
|
||||
const response = await api.get('/role-permissions/roles/1/permissions');
|
||||
console.log('✅ 获取特定角色的详细权限成功');
|
||||
console.log('权限树结构:', response.data.data ? '已构建' : '未构建');
|
||||
} catch (error) {
|
||||
console.log('❌ 获取特定角色的详细权限失败:', error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
// 测试4: 权限统计
|
||||
try {
|
||||
console.log('\n4. 测试权限统计...');
|
||||
const response = await api.get('/role-permissions/stats');
|
||||
console.log('✅ 权限统计成功');
|
||||
if (response.data.data) {
|
||||
const stats = response.data.data;
|
||||
console.log('统计信息:');
|
||||
console.log(` - 总角色数: ${stats.overview?.totalRoles || 0}`);
|
||||
console.log(` - 总权限数: ${stats.overview?.totalPermissions || 0}`);
|
||||
console.log(` - 总分配数: ${stats.overview?.totalAssignments || 0}`);
|
||||
console.log(` - 平均每角色权限数: ${stats.overview?.averagePermissionsPerRole || 0}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ 权限统计失败:', error.response?.data?.message || error.message);
|
||||
}
|
||||
|
||||
console.log('\n测试完成!');
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
testRolePermissionsAPI().catch(console.error);
|
||||
@@ -1,31 +0,0 @@
|
||||
const http = require('http');
|
||||
|
||||
// 测试健康检查接口
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: 3000,
|
||||
path: '/health',
|
||||
method: 'GET'
|
||||
};
|
||||
|
||||
console.log('正在测试健康检查接口...');
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
console.log(`状态码: ${res.statusCode}`);
|
||||
console.log(`响应头: ${JSON.stringify(res.headers)}`);
|
||||
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log('响应内容:', data);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e) => {
|
||||
console.error(`请求遇到问题: ${e.message}`);
|
||||
});
|
||||
|
||||
req.end();
|
||||
@@ -1,42 +0,0 @@
|
||||
const { sequelize } = require('./config/database');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
async function updateAdminPassword() {
|
||||
try {
|
||||
// 连接数据库
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 密码为123456,使用bcrypt进行哈希处理
|
||||
const plainPassword = '123456';
|
||||
const hashedPassword = await bcrypt.hash(plainPassword, 12);
|
||||
console.log('🔑 密码哈希生成成功');
|
||||
|
||||
// 更新admin用户的密码
|
||||
const [updatedRows] = await sequelize.query(
|
||||
'UPDATE users SET password = :password WHERE username = :username',
|
||||
{
|
||||
replacements: {
|
||||
password: hashedPassword,
|
||||
username: 'admin'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (updatedRows > 0) {
|
||||
console.log('✅ admin用户密码已更新为: 123456');
|
||||
} else {
|
||||
console.log('⚠️ 未找到admin用户,请检查数据库');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 更新密码失败:', error.message);
|
||||
} finally {
|
||||
// 关闭数据库连接
|
||||
await sequelize.close();
|
||||
console.log('🔒 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
// 执行脚本
|
||||
updateAdminPassword();
|
||||
@@ -1,117 +0,0 @@
|
||||
// app.js
|
||||
App({
|
||||
globalData: {
|
||||
version: '1.0.0',
|
||||
platform: 'wechat',
|
||||
isDevelopment: true,
|
||||
baseUrl: 'https://ad.ningmuyun.com', // 请替换为实际的后端API地址
|
||||
userInfo: null,
|
||||
token: null
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
console.log('养殖管理系统启动')
|
||||
this.initApp()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('应用显示')
|
||||
},
|
||||
|
||||
onHide() {
|
||||
console.log('应用隐藏')
|
||||
},
|
||||
|
||||
onError(error) {
|
||||
console.error('应用错误:', error)
|
||||
},
|
||||
|
||||
// 应用初始化
|
||||
async initApp() {
|
||||
try {
|
||||
// 检查登录状态
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (token && userInfo) {
|
||||
this.globalData.token = token
|
||||
this.globalData.userInfo = userInfo
|
||||
console.log('用户已登录,token:', token)
|
||||
console.log('用户信息:', userInfo)
|
||||
} else {
|
||||
console.log('用户未登录')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('应用初始化失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
// 设置用户信息
|
||||
setUserInfo(userInfo) {
|
||||
this.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
},
|
||||
|
||||
// 设置token
|
||||
setToken(token) {
|
||||
this.globalData.token = token
|
||||
wx.setStorageSync('token', token)
|
||||
},
|
||||
|
||||
// 清除用户信息
|
||||
clearUserInfo() {
|
||||
this.globalData.userInfo = null
|
||||
this.globalData.token = null
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.removeStorageSync('token')
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
return !!(this.globalData.token && this.globalData.userInfo)
|
||||
},
|
||||
|
||||
// 显示加载提示
|
||||
showLoading(title = '加载中...') {
|
||||
wx.showLoading({
|
||||
title: title,
|
||||
mask: true
|
||||
})
|
||||
},
|
||||
|
||||
// 隐藏加载提示
|
||||
hideLoading() {
|
||||
wx.hideLoading()
|
||||
},
|
||||
|
||||
// 显示成功提示
|
||||
showSuccess(title = '操作成功') {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示错误提示
|
||||
showError(title = '操作失败') {
|
||||
wx.showToast({
|
||||
title: title,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
},
|
||||
|
||||
// 显示确认对话框
|
||||
showConfirm(content, title = '提示') {
|
||||
return new Promise((resolve) => {
|
||||
wx.showModal({
|
||||
title: title,
|
||||
content: content,
|
||||
success: (res) => {
|
||||
resolve(res.confirm)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/home/home",
|
||||
"pages/production/production",
|
||||
"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",
|
||||
"pages/device/host/host",
|
||||
"pages/device/fence/fence",
|
||||
"pages/device/eartag-detail/eartag-detail",
|
||||
"pages/device/eartag-add/eartag-add",
|
||||
"pages/alert/alert"
|
||||
],
|
||||
"tabBar": {
|
||||
"color": "#7A7E83",
|
||||
"selectedColor": "#3cc51f",
|
||||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/home/home",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/production/production",
|
||||
"text": "生产管理"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTitleText": "养殖管理系统",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#f8f8f8"
|
||||
},
|
||||
"networkTimeout": {
|
||||
"request": 10000,
|
||||
"downloadFile": 10000
|
||||
},
|
||||
"debug": true,
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "你的位置信息将用于养殖场定位和地图展示"
|
||||
}
|
||||
},
|
||||
"requiredBackgroundModes": ["location"],
|
||||
"sitemapLocation": "sitemap.json"
|
||||
}
|
||||
21
mini_program/farm-monitor-dashboard/images/fence-marker.svg
Normal file
21
mini_program/farm-monitor-dashboard/images/fence-marker.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 背景圆形 -->
|
||||
<circle cx="15" cy="15" r="14" fill="#7CB342" stroke="#ffffff" stroke-width="2"/>
|
||||
|
||||
<!-- 围栏图标 -->
|
||||
<g transform="translate(7, 7)">
|
||||
<!-- 围栏柱子 -->
|
||||
<rect x="2" y="4" width="1.5" height="12" fill="#ffffff"/>
|
||||
<rect x="6" y="4" width="1.5" height="12" fill="#ffffff"/>
|
||||
<rect x="10" y="4" width="1.5" height="12" fill="#ffffff"/>
|
||||
<rect x="14" y="4" width="1.5" height="12" fill="#ffffff"/>
|
||||
|
||||
<!-- 围栏横条 -->
|
||||
<rect x="1" y="7" width="15" height="1" fill="#ffffff"/>
|
||||
<rect x="1" y="10" width="15" height="1" fill="#ffffff"/>
|
||||
<rect x="1" y="13" width="15" height="1" fill="#ffffff"/>
|
||||
</g>
|
||||
|
||||
<!-- 阴影效果 -->
|
||||
<circle cx="15" cy="16" r="12" fill="rgba(0,0,0,0.1)" opacity="0.3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 873 B |
@@ -76,6 +76,58 @@ Page({
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 当类型为耳标预警时,改用智能耳标公开接口
|
||||
if (this.data.typeFilter === 'eartag') {
|
||||
const params = {
|
||||
search: this.data.searchKeyword || '',
|
||||
alertType: '',
|
||||
page: this.data.page,
|
||||
limit: this.data.pageSize
|
||||
}
|
||||
const res = await alertApi.getEartagAlerts(params)
|
||||
let newList = []
|
||||
if (Array.isArray(res)) {
|
||||
newList = res
|
||||
} else if (res && Array.isArray(res.data)) {
|
||||
newList = res.data
|
||||
}
|
||||
// 将远程数据映射为页面展示所需结构
|
||||
const mappedList = newList.map(item => this.mapEartagAlertToListItem(item))
|
||||
const alertList = this.data.page === 1 ? mappedList : [...this.data.alertList, ...mappedList]
|
||||
this.setData({
|
||||
alertList,
|
||||
total: alertList.length,
|
||||
hasMore: newList.length === this.data.pageSize
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 当类型为项圈预警时,改用智能项圈公开接口
|
||||
if (this.data.typeFilter === 'collar') {
|
||||
const params = {
|
||||
search: this.data.searchKeyword || '',
|
||||
alertType: '',
|
||||
page: this.data.page,
|
||||
limit: this.data.pageSize
|
||||
}
|
||||
const res = await alertApi.getCollarAlerts(params)
|
||||
let newList = []
|
||||
if (Array.isArray(res)) {
|
||||
newList = res
|
||||
} else if (res && Array.isArray(res.data)) {
|
||||
newList = res.data
|
||||
}
|
||||
const mappedList = newList.map(item => this.mapCollarAlertToListItem(item))
|
||||
const alertList = this.data.page === 1 ? mappedList : [...this.data.alertList, ...mappedList]
|
||||
this.setData({
|
||||
alertList,
|
||||
total: alertList.length,
|
||||
hasMore: newList.length === this.data.pageSize
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 其他类型维持原有接口逻辑
|
||||
const params = {
|
||||
page: this.data.page,
|
||||
pageSize: this.data.pageSize,
|
||||
@@ -414,6 +466,52 @@ Page({
|
||||
return colorMap[priority] || '#909399'
|
||||
},
|
||||
|
||||
// 将远程耳标预警数据转换为页面列表所需字段
|
||||
mapEartagAlertToListItem(item) {
|
||||
const titleMap = {
|
||||
temperature: '温度异常',
|
||||
movement: '运动量异常',
|
||||
position: '位置异常',
|
||||
battery: '电量偏低'
|
||||
}
|
||||
const titleBase = titleMap[item.alertType] || '耳标预警'
|
||||
const levelText = item.alertLevel ? `(${item.alertLevel})` : ''
|
||||
return {
|
||||
id: item.id || `${item.deviceId || 'unknown'}_${item.alertType || 'eartag'}`,
|
||||
type: 'eartag',
|
||||
title: `${titleBase}${levelText}`,
|
||||
content: item.description || '',
|
||||
deviceName: item.deviceName || item.eartagNumber || '未知',
|
||||
createTime: item.alertTime || item.createTime || '',
|
||||
status: 'pending',
|
||||
priority: item.alertLevel || 'medium'
|
||||
}
|
||||
},
|
||||
|
||||
// 将远程项圈预警数据转换为页面列表所需字段
|
||||
mapCollarAlertToListItem(item) {
|
||||
const titleMap = {
|
||||
temperature: '温度异常',
|
||||
movement: '运动量异常',
|
||||
battery: '电量偏低',
|
||||
offline: '设备离线',
|
||||
gps: '定位异常',
|
||||
wear: '佩戴异常'
|
||||
}
|
||||
const titleBase = titleMap[item.alertType] || '项圈预警'
|
||||
const levelText = item.alertLevel ? `(${item.alertLevel})` : ''
|
||||
return {
|
||||
id: item.id || `${item.deviceId || 'unknown'}_${item.alertType || 'collar'}`,
|
||||
type: 'collar',
|
||||
title: `${titleBase}${levelText}`,
|
||||
content: item.description || '',
|
||||
deviceName: item.deviceName || item.collarNumber || '未知',
|
||||
createTime: item.alertTime || item.createTime || '',
|
||||
status: 'pending',
|
||||
priority: item.alertLevel || 'medium'
|
||||
}
|
||||
},
|
||||
|
||||
// 映射预警详情字段为键值对用于展示
|
||||
mapAlertDetail(type, d) {
|
||||
if (!d) return []
|
||||
@@ -458,11 +556,21 @@ Page({
|
||||
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: '项圈编号', value: d.collarNumber || d.deviceName || '-' })
|
||||
pairs.push({ label: '设备ID', value: d.deviceId || '-' })
|
||||
pairs.push({ label: '设备名称', value: d.deviceName || '-' })
|
||||
if (d.battery !== undefined) pairs.push({ label: '电量(%)', value: d.battery })
|
||||
pairs.push({ label: '设备状态', value: d.deviceStatus || '-' })
|
||||
if (d.wearStatus) pairs.push({ label: '佩戴状态', value: d.wearStatus })
|
||||
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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "牛只批次设置",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
})
|
||||
@@ -1,285 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "栏舍设置",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
})
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
|
||||
// 状态映射
|
||||
const statusMap = {
|
||||
'在线': '在线',
|
||||
'离线': '离线',
|
||||
'告警': '告警',
|
||||
'online': '在线',
|
||||
'offline': '离线',
|
||||
'alarm': '告警'
|
||||
}
|
||||
|
||||
// 佩戴状态映射
|
||||
const wearStatusMap = {
|
||||
1: '已佩戴',
|
||||
0: '未佩戴'
|
||||
}
|
||||
|
||||
// 连接状态映射
|
||||
const connectStatusMap = {
|
||||
1: '已连接',
|
||||
0: '未连接'
|
||||
}
|
||||
|
||||
// 设备状态映射
|
||||
const deviceStatusMap = {
|
||||
'使用中': '使用中',
|
||||
'待机': '待机',
|
||||
'维护': '维护',
|
||||
'故障': '故障'
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
list: [], // 项圈数据列表
|
||||
searchValue: '', // 搜索值
|
||||
currentPage: 1, // 当前页码
|
||||
total: 0, // 总数据量
|
||||
pageSize: 10, // 每页数量
|
||||
totalPages: 0, // 总页数
|
||||
pageNumbers: [], // 页码数组
|
||||
isSearching: false, // 是否在搜索状态
|
||||
searchResult: null // 搜索结果
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 检查登录状态
|
||||
if (!this.checkLoginStatus()) {
|
||||
return
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (!token || !userInfo) {
|
||||
console.log('用户未登录,跳转到登录页')
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再使用',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('用户已登录:', userInfo.username)
|
||||
return true
|
||||
},
|
||||
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const { currentPage, pageSize, searchValue } = this.data
|
||||
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')
|
||||
if (!token) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
wx.request({
|
||||
url,
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('API响应:', res)
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
// 处理API响应格式
|
||||
const response = res.data
|
||||
console.log('API响应数据:', response)
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 处理 {success: true, data: [...]} 格式
|
||||
const data = response.data
|
||||
const total = response.total || response.pagination?.total || 0
|
||||
const totalPages = Math.ceil(total / this.data.pageSize)
|
||||
const pageNumbers = this.generatePageNumbers(this.data.currentPage, totalPages)
|
||||
|
||||
this.setData({
|
||||
list: Array.isArray(data) ? data.map(item => this.formatItemData(item)) : [],
|
||||
total: total,
|
||||
totalPages: totalPages,
|
||||
pageNumbers: pageNumbers
|
||||
})
|
||||
} else if (response.data && response.data.items) {
|
||||
// 处理 {data: {items: [...]}} 格式
|
||||
const data = response.data
|
||||
this.setData({
|
||||
list: (data.items || []).map(item => this.formatItemData(item)),
|
||||
total: data.total || 0
|
||||
})
|
||||
} else {
|
||||
// 直接数组格式
|
||||
this.setData({
|
||||
list: Array.isArray(response) ? response.map(item => this.formatItemData(item)) : [],
|
||||
total: response.length || 0
|
||||
})
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
// 处理401未授权
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
// 清除本地存储
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: res.data?.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('请求失败:', err)
|
||||
wx.showToast({
|
||||
title: err.errMsg?.includes('401') ? '请登录后重试' : '网络请求失败',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
wx.hideLoading()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化单个设备数据
|
||||
formatItemData(item) {
|
||||
return {
|
||||
...item,
|
||||
// 状态映射
|
||||
statusText: statusMap[item.status] || item.status || '在线',
|
||||
// 佩戴状态映射
|
||||
wearStatusText: wearStatusMap[item.is_wear] || wearStatusMap[item.bandge_status] || '未知',
|
||||
// 连接状态映射
|
||||
connectStatusText: connectStatusMap[item.is_connect] || '未知',
|
||||
// 设备状态映射
|
||||
deviceStatusText: deviceStatusMap[item.deviceStatus] || item.deviceStatus || '未知',
|
||||
// 格式化电池电量
|
||||
batteryText: `${item.battery || item.batteryPercent || 0}%`,
|
||||
// 格式化温度
|
||||
temperatureText: `${item.temperature || item.raw?.temperature_raw || '0'}°C`,
|
||||
// 格式化步数
|
||||
stepsText: `${item.steps || 0}步`,
|
||||
// 格式化信号强度
|
||||
signalText: item.rsrp ? `${item.rsrp}dBm` : '未知',
|
||||
// 格式化GPS信号
|
||||
gpsText: item.gpsSignal ? `${item.gpsSignal}颗卫星` : '无信号',
|
||||
// 格式化位置信息
|
||||
locationText: item.location || (item.longitude && item.latitude ? '有定位' : '无定位'),
|
||||
// 格式化最后更新时间
|
||||
lastUpdateText: item.lastUpdate || '未知',
|
||||
// 格式化设备序列号
|
||||
snText: item.sn ? `SN:${item.sn}` : '未知',
|
||||
// 格式化更新间隔
|
||||
updateIntervalText: item.updateInterval ? `${Math.floor(item.updateInterval / 1000)}秒` : '未知'
|
||||
}
|
||||
},
|
||||
|
||||
// 生成页码数组
|
||||
generatePageNumbers(currentPage, totalPages) {
|
||||
const pageNumbers = []
|
||||
const maxVisible = 5 // 最多显示5个页码
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
// 总页数少于等于5页,显示所有页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pageNumbers.push(i)
|
||||
}
|
||||
} else {
|
||||
// 总页数大于5页,智能显示页码
|
||||
let start = Math.max(1, currentPage - 2)
|
||||
let end = Math.min(totalPages, start + maxVisible - 1)
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1)
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pageNumbers.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
return pageNumbers
|
||||
},
|
||||
|
||||
// 上一页
|
||||
onPrevPage() {
|
||||
if (this.data.currentPage > 1) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage - 1
|
||||
}, () => {
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
onNextPage() {
|
||||
if (this.data.currentPage < this.data.totalPages) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage + 1
|
||||
}, () => {
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchValue: e.detail.value.trim() })
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
const searchValue = this.data.searchValue.trim()
|
||||
if (!searchValue) {
|
||||
wx.showToast({
|
||||
title: '请输入项圈编号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证项圈编号格式(数字)
|
||||
if (!/^\d+$/.test(searchValue)) {
|
||||
wx.showToast({
|
||||
title: '项圈编号只能包含数字',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置搜索状态
|
||||
this.setData({
|
||||
isSearching: true,
|
||||
searchResult: null,
|
||||
currentPage: 1
|
||||
})
|
||||
|
||||
// 执行精确搜索
|
||||
this.performExactSearch(searchValue)
|
||||
},
|
||||
|
||||
// 执行精确搜索
|
||||
performExactSearch(searchValue) {
|
||||
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')
|
||||
if (!token) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '搜索中...' })
|
||||
wx.request({
|
||||
url,
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('搜索API响应:', res)
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const response = res.data
|
||||
|
||||
if (response.success && response.data) {
|
||||
const data = response.data
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
// 找到匹配的设备
|
||||
const device = this.formatItemData(data[0])
|
||||
this.setData({
|
||||
searchResult: device,
|
||||
list: [], // 清空列表显示
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
pageNumbers: [1]
|
||||
})
|
||||
wx.showToast({
|
||||
title: '搜索成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
// 没有找到匹配的设备
|
||||
this.setData({
|
||||
searchResult: null,
|
||||
list: [],
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
pageNumbers: []
|
||||
})
|
||||
wx.showToast({
|
||||
title: '未找到该设备',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '搜索失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '搜索失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('搜索请求失败:', err)
|
||||
wx.showToast({
|
||||
title: '网络请求失败',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
wx.hideLoading()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 清除搜索
|
||||
clearSearch() {
|
||||
this.setData({
|
||||
searchValue: '',
|
||||
isSearching: false,
|
||||
searchResult: null,
|
||||
currentPage: 1
|
||||
})
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 分页切换
|
||||
onPageChange(e) {
|
||||
const page = parseInt(e.currentTarget.dataset.page)
|
||||
if (page !== this.data.currentPage) {
|
||||
this.setData({ currentPage: page }, () => this.loadData())
|
||||
}
|
||||
},
|
||||
|
||||
// 计算分页数组
|
||||
getPages(total, pageSize, currentPage) {
|
||||
const pageCount = Math.ceil(total / pageSize)
|
||||
return Array.from({ length: pageCount }, (_, i) => i + 1)
|
||||
}
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationBarTitleText": "智能项圈管理"
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
// pages/device/device.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { formatDate, formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
deviceList: [],
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
searchKeyword: '',
|
||||
typeFilter: 'all',
|
||||
statusFilter: 'all',
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
total: 0,
|
||||
deviceTypes: [
|
||||
{ value: 'eartag', label: '耳标', icon: '🏷️' },
|
||||
{ value: 'collar', label: '项圈', icon: '📱' },
|
||||
{ value: 'ankle', label: '脚环', icon: '⌚' },
|
||||
{ value: 'host', label: '主机', icon: '📡' }
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.data.hasMore && !this.data.loading) {
|
||||
this.loadMoreDevices()
|
||||
}
|
||||
},
|
||||
|
||||
// 加载设备列表
|
||||
async loadDeviceList() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: this.data.page,
|
||||
pageSize: this.data.pageSize,
|
||||
type: this.data.typeFilter === 'all' ? '' : this.data.typeFilter,
|
||||
status: this.data.statusFilter === 'all' ? '' : this.data.statusFilter
|
||||
}
|
||||
|
||||
if (this.data.searchKeyword) {
|
||||
params.search = this.data.searchKeyword
|
||||
}
|
||||
|
||||
const response = await get('/devices', params)
|
||||
|
||||
if (response.success) {
|
||||
const newList = response.data.list || []
|
||||
const deviceList = this.data.page === 1 ? newList : [...this.data.deviceList, ...newList]
|
||||
|
||||
this.setData({
|
||||
deviceList,
|
||||
total: response.data.total || 0,
|
||||
hasMore: deviceList.length < (response.data.total || 0)
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
wx.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 加载更多设备
|
||||
async loadMoreDevices() {
|
||||
if (!this.data.hasMore || this.data.loading) return
|
||||
|
||||
this.setData({
|
||||
page: this.data.page + 1
|
||||
})
|
||||
|
||||
await this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
onClearSearch() {
|
||||
this.setData({
|
||||
searchKeyword: '',
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 类型筛选
|
||||
onTypeFilter(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({
|
||||
typeFilter: type,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 状态筛选
|
||||
onStatusFilter(e) {
|
||||
const status = e.currentTarget.dataset.status
|
||||
this.setData({
|
||||
statusFilter: status,
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
},
|
||||
|
||||
// 查看设备详情
|
||||
viewDeviceDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
|
||||
let url = ''
|
||||
switch (type) {
|
||||
case 'eartag':
|
||||
url = `/pages/device/eartag/eartag?id=${id}`
|
||||
break
|
||||
case 'collar':
|
||||
url = `/pages/device/collar/collar?id=${id}`
|
||||
break
|
||||
case 'ankle':
|
||||
url = `/pages/device/ankle/ankle?id=${id}`
|
||||
break
|
||||
case 'host':
|
||||
url = `/pages/device/host/host?id=${id}`
|
||||
break
|
||||
default:
|
||||
wx.showToast({
|
||||
title: '未知设备类型',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.navigateTo({ url })
|
||||
},
|
||||
|
||||
// 添加设备
|
||||
addDevice() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['耳标', '项圈', '脚环', '主机'],
|
||||
success: (res) => {
|
||||
const types = ['eartag', 'collar', 'ankle', 'host']
|
||||
const type = types[res.tapIndex]
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/add/add?type=${type}`
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 编辑设备
|
||||
editDevice(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const type = e.currentTarget.dataset.type
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/edit/edit?id=${id}&type=${type}`
|
||||
})
|
||||
},
|
||||
|
||||
// 删除设备
|
||||
async deleteDevice(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const name = e.currentTarget.dataset.name
|
||||
|
||||
const confirmed = await wx.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除设备"${name}"吗?`,
|
||||
confirmText: '删除',
|
||||
confirmColor: '#f5222d'
|
||||
})
|
||||
|
||||
if (confirmed) {
|
||||
try {
|
||||
wx.showLoading({ title: '删除中...' })
|
||||
|
||||
const response = await del(`/devices/${id}`)
|
||||
|
||||
if (response.success) {
|
||||
wx.showToast({
|
||||
title: '删除成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 刷新列表
|
||||
this.setData({
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
deviceList: []
|
||||
})
|
||||
this.loadDeviceList()
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除设备失败:', error)
|
||||
wx.showToast({
|
||||
title: '删除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取设备类型信息
|
||||
getDeviceTypeInfo(type) {
|
||||
return this.data.deviceTypes.find(item => item.value === type) || { label: '未知', icon: '❓' }
|
||||
},
|
||||
|
||||
// 获取状态文本
|
||||
getStatusText(status) {
|
||||
const statusMap = {
|
||||
'online': '在线',
|
||||
'offline': '离线',
|
||||
'error': '故障',
|
||||
'maintenance': '维护中'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
},
|
||||
|
||||
// 获取状态颜色
|
||||
getStatusColor(status) {
|
||||
const colorMap = {
|
||||
'online': '#52c41a',
|
||||
'offline': '#909399',
|
||||
'error': '#f5222d',
|
||||
'maintenance': '#faad14'
|
||||
}
|
||||
return colorMap[status] || '#909399'
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(date) {
|
||||
return formatDate(date)
|
||||
},
|
||||
|
||||
// 格式化时间
|
||||
formatTime(time) {
|
||||
return formatTime(time)
|
||||
}
|
||||
})
|
||||
@@ -1,106 +0,0 @@
|
||||
// pages/device/eartag-add/eartag-add.js
|
||||
const { post } = require('../../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
formData: {
|
||||
eartagNumber: '',
|
||||
hostNumber: '',
|
||||
batteryLevel: '',
|
||||
remark: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 页面加载时的初始化
|
||||
},
|
||||
|
||||
// 耳标编号输入
|
||||
onEartagNumberInput(e) {
|
||||
this.setData({
|
||||
'formData.eartagNumber': e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 主机号输入
|
||||
onHostNumberInput(e) {
|
||||
this.setData({
|
||||
'formData.hostNumber': e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 电量输入
|
||||
onBatteryInput(e) {
|
||||
this.setData({
|
||||
'formData.batteryLevel': e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 备注输入
|
||||
onRemarkInput(e) {
|
||||
this.setData({
|
||||
'formData.remark': e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
// 提交表单
|
||||
async onSubmit() {
|
||||
const { formData } = this.data
|
||||
|
||||
// 验证表单
|
||||
if (!formData.eartagNumber.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入耳标编号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.hostNumber.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入主机号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 调用添加耳标API
|
||||
const response = await post('/api/iot-jbq-client', {
|
||||
eartagNumber: formData.eartagNumber,
|
||||
hostNumber: formData.hostNumber,
|
||||
batteryLevel: formData.batteryLevel || 100,
|
||||
remark: formData.remark
|
||||
})
|
||||
|
||||
console.log('添加耳标成功:', response)
|
||||
|
||||
wx.showToast({
|
||||
title: '添加成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 延迟返回上一页
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
|
||||
} catch (error) {
|
||||
console.error('添加耳标失败:', error)
|
||||
wx.showToast({
|
||||
title: '添加失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,309 +0,0 @@
|
||||
// pages/device/eartag-detail/eartag-detail.js
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
eartagId: '',
|
||||
eartagData: {}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
console.log('耳标详情页参数:', options)
|
||||
|
||||
if (options.id) {
|
||||
this.setData({ eartagId: options.id })
|
||||
this.fetchEartagDetail(options.id)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '参数错误',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取耳标详情
|
||||
async fetchEartagDetail(eartagId) {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 获取token
|
||||
const token = wx.getStorageSync('token')
|
||||
console.log('详情页获取token:', token)
|
||||
|
||||
if (!token) {
|
||||
console.log('未找到token,跳转到登录页')
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证token格式
|
||||
if (!token.startsWith('eyJ')) {
|
||||
console.log('token格式不正确,清除并重新登录')
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用私有API获取耳标列表,然后筛选出指定耳标
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
limit: 1000, // 获取足够多的数据
|
||||
refresh: true,
|
||||
_t: Date.now()
|
||||
},
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: 10000,
|
||||
success: (res) => {
|
||||
console.log('耳标列表API调用成功:', res)
|
||||
if (res.statusCode === 401) {
|
||||
console.log('收到401错误,清除token并跳转登录页')
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
reject(new Error('未授权,请重新登录'))
|
||||
} else {
|
||||
resolve(res)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('耳标列表API调用失败:', error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log('耳标列表API响应:', response)
|
||||
|
||||
if (response.statusCode === 200 && response.data.success) {
|
||||
// 从列表中查找指定的耳标
|
||||
const eartagList = response.data.data || []
|
||||
const targetEartag = eartagList.find(item =>
|
||||
item.cid == eartagId ||
|
||||
item.eartagNumber == eartagId ||
|
||||
item.sn == eartagId ||
|
||||
item.id == eartagId
|
||||
)
|
||||
|
||||
if (targetEartag) {
|
||||
// 处理API数据
|
||||
const processedData = this.processEartagDetailData(targetEartag)
|
||||
console.log('处理后的详情数据:', processedData)
|
||||
this.setData({ eartagData: processedData })
|
||||
} else {
|
||||
throw new Error('未找到指定的耳标')
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.data.message || '获取详情失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取耳标详情失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 处理耳标详情数据
|
||||
processEartagDetailData(apiData) {
|
||||
try {
|
||||
console.log('处理耳标详情数据,原始数据:', apiData)
|
||||
|
||||
const processedData = {
|
||||
eartagNumber: apiData.cid || apiData.eartagNumber || apiData.sn || apiData.id || '未知',
|
||||
isBound: this.checkIfBound(apiData),
|
||||
batteryLevel: this.formatBatteryLevel(apiData),
|
||||
temperature: this.formatTemperature(apiData),
|
||||
hostNumber: this.formatHostNumber(apiData),
|
||||
totalMovement: this.formatMovement(apiData.totalMovement || apiData.dailyMovement || apiData.walk || apiData.steps || 0),
|
||||
todayMovement: this.formatMovement(apiData.dailyMovement || apiData['walk-y_steps'] || apiData.todayMovement || 0),
|
||||
updateTime: this.formatUpdateTime(apiData.lastUpdate || apiData.updateTime || new Date().toISOString()),
|
||||
// 新增字段
|
||||
wearStatus: apiData.wearStatus || '未知',
|
||||
deviceStatus: apiData.deviceStatus || '未知',
|
||||
gpsState: apiData.gps_state || '未知',
|
||||
location: apiData.location || '无定位',
|
||||
latitude: apiData.lat || '0',
|
||||
longitude: apiData.lon || '0',
|
||||
bindingStatus: apiData.bindingStatus || '未知',
|
||||
voltage: apiData.voltage || '0',
|
||||
rawData: apiData // 保存原始数据用于调试
|
||||
}
|
||||
|
||||
console.log('处理后的详情数据:', processedData)
|
||||
return processedData
|
||||
} catch (error) {
|
||||
console.error('处理耳标详情数据失败:', error)
|
||||
return {
|
||||
eartagNumber: '处理失败',
|
||||
isBound: false,
|
||||
batteryLevel: 0,
|
||||
temperature: '0.0',
|
||||
hostNumber: '未知',
|
||||
totalMovement: '0',
|
||||
todayMovement: '0',
|
||||
updateTime: '未知时间',
|
||||
wearStatus: '未知',
|
||||
deviceStatus: '未知',
|
||||
gpsState: '未知',
|
||||
location: '无定位',
|
||||
latitude: '0',
|
||||
longitude: '0',
|
||||
bindingStatus: '未知',
|
||||
voltage: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 检查是否已绑定
|
||||
checkIfBound(item) {
|
||||
if (item.bindingStatus === '已绑定') return true
|
||||
if (item.bindingStatus === '未绑定') return false
|
||||
if (item.isBound !== undefined) return item.isBound
|
||||
if (item.is_bound !== undefined) return item.is_bound
|
||||
if (item.bound !== undefined) return item.bound
|
||||
if (item.bandge_status !== undefined) return item.bandge_status === 1
|
||||
if (item.is_wear !== undefined) return item.is_wear === 1
|
||||
if (item.status === 'bound' || item.status === '已绑定') return true
|
||||
if (item.status === 'unbound' || item.status === '未绑定') return false
|
||||
return !!(item.cattleId || item.cattle_id || item.animalId || item.animal_id)
|
||||
},
|
||||
|
||||
// 格式化电量
|
||||
formatBatteryLevel(item) {
|
||||
const battery = item.voltage || item.battery || item.batteryPercent || item.batteryLevel || item.battery_level || item.power || 0
|
||||
return Math.round(parseFloat(battery)) || 0
|
||||
},
|
||||
|
||||
// 格式化温度
|
||||
formatTemperature(item) {
|
||||
const temp = item.temperature || item.temp || item.device_temp || 0
|
||||
return parseFloat(temp).toFixed(1) || '0.0'
|
||||
},
|
||||
|
||||
// 格式化主机号
|
||||
formatHostNumber(item) {
|
||||
return item.sid || item.collectedHost || item.deviceInfo || item.hostNumber || item.host_number || item.hostId || item.host_id || item.collector || '未知主机'
|
||||
},
|
||||
|
||||
// 格式化运动量
|
||||
formatMovement(movement) {
|
||||
const value = parseInt(movement) || 0
|
||||
return value.toString()
|
||||
},
|
||||
|
||||
// 格式化更新时间
|
||||
formatUpdateTime(timeStr) {
|
||||
if (!timeStr) return '未知时间'
|
||||
|
||||
try {
|
||||
const date = new Date(timeStr)
|
||||
if (isNaN(date.getTime())) return timeStr
|
||||
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).replace(/\//g, '-')
|
||||
} catch (error) {
|
||||
return timeStr
|
||||
}
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
// 绑定牛只
|
||||
onBind() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/cattle/bind-cattle?eartagId=${this.data.eartagId}`
|
||||
})
|
||||
},
|
||||
|
||||
// 解绑牛只
|
||||
onUnbind() {
|
||||
wx.showModal({
|
||||
title: '确认解绑',
|
||||
content: '确定要解绑此耳标吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 调用解绑API
|
||||
this.unbindEartag()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 解绑耳标
|
||||
async unbindEartag() {
|
||||
try {
|
||||
// 调用解绑API
|
||||
// await post('/api/iot-jbq-client/unbind', { eartagId: this.data.eartagId })
|
||||
|
||||
wx.showToast({
|
||||
title: '解绑成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 更新数据
|
||||
const eartagData = { ...this.data.eartagData, isBound: false }
|
||||
this.setData({ eartagData })
|
||||
|
||||
} catch (error) {
|
||||
console.error('解绑失败:', error)
|
||||
wx.showToast({
|
||||
title: '解绑失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 编辑信息
|
||||
onEdit() {
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/eartag-edit/eartag-edit?id=${this.data.eartagId}`
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,800 +0,0 @@
|
||||
// pages/device/eartag/eartag.js
|
||||
const { get } = require('../../../utils/api')
|
||||
const { formatTime } = require('../../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false, // 初始状态设为未加载
|
||||
searchKeyword: '',
|
||||
currentFilter: 'all', // all, bound, unbound
|
||||
filterTabs: [
|
||||
{ name: '耳标总数', type: 'all', count: 0, active: true },
|
||||
{ name: '已绑定数量', type: 'bound', count: 0, active: false },
|
||||
{ name: '未绑定数量', type: 'unbound', count: 0, active: false }
|
||||
],
|
||||
allEartagList: [], // 所有耳标数据
|
||||
eartagList: [], // 当前显示的耳标列表
|
||||
originalData: [], // 原始API数据
|
||||
|
||||
// 分页相关
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
totalPages: 0,
|
||||
totalCount: 0,
|
||||
paginationList: [] // 分页页码列表
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('eartag页面onLoad执行')
|
||||
// 页面加载时获取数据
|
||||
this.fetchEartagData()
|
||||
},
|
||||
|
||||
onReady() {
|
||||
console.log('eartag页面onReady执行')
|
||||
// 页面加载完成
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('eartag页面onShow执行')
|
||||
// 页面显示时检查是否需要刷新数据
|
||||
// 避免重复加载,只在必要时刷新
|
||||
if (this.data.currentPage === 1 && this.data.eartagList.length === 0) {
|
||||
console.log('onShow中调用fetchEartagData')
|
||||
this.fetchEartagData()
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchEartagData().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
// 获取耳标数据
|
||||
async fetchEartagData(page = 1) {
|
||||
console.log('fetchEartagData函数被调用,page:', page)
|
||||
|
||||
// 防止重复请求
|
||||
if (this.data.loading) {
|
||||
console.log('正在加载中,跳过重复请求')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('设置loading状态为true')
|
||||
this.setData({ loading: true, currentPage: page })
|
||||
|
||||
try {
|
||||
// 使用真实的智能耳标API接口(公开API,无需认证)
|
||||
console.log('开始调用API...')
|
||||
|
||||
// 使用私有API端点,需要认证
|
||||
const token = wx.getStorageSync('token')
|
||||
console.log('当前token:', token)
|
||||
console.log('token是否存在:', !!token)
|
||||
console.log('token长度:', token ? token.length : 0)
|
||||
|
||||
if (!token) {
|
||||
console.log('未找到token,跳转到登录页')
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证token格式
|
||||
if (!token.startsWith('eyJ')) {
|
||||
console.log('token格式不正确,清除并重新登录')
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
const requestHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
console.log('请求头:', requestHeaders)
|
||||
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: page,
|
||||
limit: this.data.pageSize,
|
||||
refresh: true,
|
||||
_t: Date.now()
|
||||
},
|
||||
header: requestHeaders,
|
||||
timeout: 10000,
|
||||
success: (res) => {
|
||||
console.log('私有API调用成功:', res)
|
||||
if (res.statusCode === 401) {
|
||||
console.log('收到401错误,清除token并跳转登录页')
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
reject(new Error('未授权,请重新登录'))
|
||||
} else {
|
||||
resolve(res)
|
||||
}
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('私有API调用失败:', error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
console.log('API响应成功:', response)
|
||||
|
||||
// 处理私有API数据 - 从response.data.data中获取数组数据
|
||||
console.log('完整响应结构:', response.data)
|
||||
console.log('实际数据部分:', response.data?.data)
|
||||
console.log('原始数据列表:', response.data?.data)
|
||||
const processedData = this.processApiData(response.data?.data || [])
|
||||
console.log('处理后的数据:', processedData)
|
||||
|
||||
// 计算分页信息
|
||||
const totalCount = response.data?.total || response.data?.stats?.total || 0
|
||||
const totalPages = Math.ceil(totalCount / this.data.pageSize)
|
||||
const paginationList = this.generatePaginationList(page, totalPages)
|
||||
|
||||
console.log('分页信息:', {
|
||||
totalCount: totalCount,
|
||||
totalPages: totalPages,
|
||||
currentPage: page,
|
||||
pageSize: this.data.pageSize
|
||||
})
|
||||
|
||||
this.setData({
|
||||
originalData: response,
|
||||
allEartagList: processedData,
|
||||
eartagList: processedData,
|
||||
totalCount: totalCount,
|
||||
totalPages: totalPages,
|
||||
paginationList: paginationList
|
||||
})
|
||||
|
||||
// 更新筛选标签计数 - 使用原始数据列表
|
||||
this.updateFilterCounts(response.data?.data || [])
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取耳标数据失败:', error)
|
||||
|
||||
// 根据错误类型显示不同的提示
|
||||
let errorMessage = '获取数据失败'
|
||||
if (error.message === '请求超时') {
|
||||
errorMessage = '网络超时,请检查网络连接'
|
||||
} else if (error.message && error.message.includes('timeout')) {
|
||||
errorMessage = '请求超时,请重试'
|
||||
} else if (error.message && error.message.includes('fail')) {
|
||||
errorMessage = '网络连接失败'
|
||||
}
|
||||
|
||||
wx.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 如果是第一页加载失败,设置空数据状态
|
||||
if (page === 1) {
|
||||
this.setData({
|
||||
eartagList: [],
|
||||
allEartagList: [],
|
||||
totalCount: 0,
|
||||
totalPages: 0,
|
||||
paginationList: []
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 处理API数据,进行字段映射和中文转换
|
||||
processApiData(apiData) {
|
||||
try {
|
||||
if (!apiData || !Array.isArray(apiData)) {
|
||||
console.log('API数据为空或不是数组:', apiData)
|
||||
return []
|
||||
}
|
||||
|
||||
const processedData = apiData.map((item, index) => {
|
||||
try {
|
||||
// 调试:显示完整API字段结构
|
||||
if (index === 0) {
|
||||
console.log('完整API字段结构调试:', {
|
||||
'cid (耳标编号)': item.cid,
|
||||
'voltage (电量)': item.voltage,
|
||||
'sid (主机号)': item.sid,
|
||||
'totalMovement (总运动量)': item.totalMovement,
|
||||
'dailyMovement (今日运动量)': item.dailyMovement,
|
||||
'temperature (温度)': item.temperature,
|
||||
'bindingStatus (绑定状态)': item.bindingStatus,
|
||||
'wearStatus (佩戴状态)': item.wearStatus,
|
||||
'deviceStatus (设备状态)': item.deviceStatus,
|
||||
'gps_state (GPS状态)': item.gps_state,
|
||||
'location (位置信息)': item.location,
|
||||
'lat (纬度)': item.lat,
|
||||
'lon (经度)': item.lon,
|
||||
'lastUpdate (更新时间)': item.lastUpdate
|
||||
})
|
||||
}
|
||||
|
||||
// 字段映射和中文转换 - 根据完整API字段结构
|
||||
const processedItem = {
|
||||
eartagNumber: item.cid || item.eartagNumber || item.sn || item.eartag_number || item.id || `EARTAG_${index + 1}`,
|
||||
isBound: this.checkIfBound(item),
|
||||
batteryLevel: this.formatBatteryLevel(item),
|
||||
temperature: this.formatTemperature(item),
|
||||
hostNumber: this.formatHostNumber(item),
|
||||
totalMovement: this.formatMovement(item.totalMovement || item.dailyMovement || item.walk || item.steps || item.total_movement || item.movement_total || 0),
|
||||
todayMovement: this.formatTodayMovement(item),
|
||||
updateTime: this.formatUpdateTime(item.lastUpdate || item.updateTime || item.update_time || item.last_update || new Date().toISOString()),
|
||||
// 新增字段
|
||||
wearStatus: item.wearStatus || '未知',
|
||||
deviceStatus: item.deviceStatus || '未知',
|
||||
gpsState: item.gps_state || '未知',
|
||||
location: item.location || '无定位',
|
||||
latitude: item.lat || '0',
|
||||
longitude: item.lon || '0',
|
||||
rawData: item // 保存原始数据用于调试
|
||||
}
|
||||
|
||||
// 调试:显示字段映射结果
|
||||
if (index === 0) {
|
||||
console.log('字段映射结果调试:', {
|
||||
'eartagNumber': processedItem.eartagNumber,
|
||||
'batteryLevel': processedItem.batteryLevel,
|
||||
'temperature': processedItem.temperature,
|
||||
'hostNumber': processedItem.hostNumber,
|
||||
'totalMovement': processedItem.totalMovement,
|
||||
'todayMovement': processedItem.todayMovement,
|
||||
'wearStatus': processedItem.wearStatus,
|
||||
'deviceStatus': processedItem.deviceStatus,
|
||||
'location': processedItem.location,
|
||||
'updateTime': processedItem.updateTime
|
||||
})
|
||||
}
|
||||
|
||||
return processedItem
|
||||
} catch (itemError) {
|
||||
console.error('处理单个数据项失败:', item, itemError)
|
||||
return {
|
||||
eartagNumber: `ERROR_${index + 1}`,
|
||||
isBound: false,
|
||||
batteryLevel: 0,
|
||||
temperature: '0.0',
|
||||
hostNumber: '错误',
|
||||
totalMovement: '0',
|
||||
todayMovement: '0',
|
||||
updateTime: '错误',
|
||||
rawData: item
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('数据处理完成,共处理', processedData.length, '条数据')
|
||||
return processedData
|
||||
} catch (error) {
|
||||
console.error('数据处理失败:', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
// 检查是否已绑定
|
||||
checkIfBound(item) {
|
||||
// 根据真实API字段判断绑定状态
|
||||
if (item.bindingStatus === '已绑定') return true
|
||||
if (item.bindingStatus === '未绑定') return false
|
||||
if (item.isBound !== undefined) return item.isBound
|
||||
if (item.is_bound !== undefined) return item.is_bound
|
||||
if (item.bound !== undefined) return item.bound
|
||||
if (item.bandge_status !== undefined) return item.bandge_status === 1
|
||||
if (item.is_wear !== undefined) return item.is_wear === 1
|
||||
if (item.status === 'bound' || item.status === '已绑定') return true
|
||||
if (item.status === 'unbound' || item.status === '未绑定') return false
|
||||
|
||||
// 默认根据是否有牛只ID判断
|
||||
return !!(item.cattleId || item.cattle_id || item.animalId || item.animal_id)
|
||||
},
|
||||
|
||||
// 格式化电量 - 优先使用voltage字段(完整API字段)
|
||||
formatBatteryLevel(item) {
|
||||
const battery = item.voltage || item.battery || item.batteryPercent || item.batteryLevel || item.battery_level || item.power || 0
|
||||
return Math.round(parseFloat(battery)) || 0
|
||||
},
|
||||
|
||||
// 格式化温度 - 使用temperature字段
|
||||
formatTemperature(item) {
|
||||
const temp = item.temperature || item.temp || item.device_temp || 0
|
||||
return parseFloat(temp).toFixed(1) || '0.0'
|
||||
},
|
||||
|
||||
// 格式化主机号 - 优先使用sid字段(完整API字段)
|
||||
formatHostNumber(item) {
|
||||
return item.sid || item.collectedHost || item.deviceInfo || item.hostNumber || item.host_number || item.hostId || item.host_id || item.collector || '未知主机'
|
||||
},
|
||||
|
||||
// 格式化运动量
|
||||
formatMovement(movement) {
|
||||
const value = parseInt(movement) || 0
|
||||
return value.toString()
|
||||
},
|
||||
|
||||
// 格式化今日运动量 - 优先使用dailyMovement字段(完整API字段)
|
||||
formatTodayMovement(item) {
|
||||
const todayMovement = item.dailyMovement || item['walk-y_steps'] || item.todayMovement || item.today_movement || item.movement_today || 0
|
||||
return this.formatMovement(todayMovement)
|
||||
},
|
||||
|
||||
// 生成分页页码列表
|
||||
generatePaginationList(currentPage, totalPages) {
|
||||
const paginationList = []
|
||||
const maxVisiblePages = 5 // 最多显示5个页码
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
// 总页数少于等于5页,显示所有页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 总页数大于5页,显示省略号
|
||||
if (currentPage <= 3) {
|
||||
// 当前页在前3页
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
paginationList.push({
|
||||
page: totalPages,
|
||||
active: false,
|
||||
text: totalPages.toString()
|
||||
})
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
// 当前页在后3页
|
||||
paginationList.push({
|
||||
page: 1,
|
||||
active: false,
|
||||
text: '1'
|
||||
})
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
for (let i = totalPages - 3; i <= totalPages; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 当前页在中间
|
||||
paginationList.push({
|
||||
page: 1,
|
||||
active: false,
|
||||
text: '1'
|
||||
})
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
paginationList.push({
|
||||
page: totalPages,
|
||||
active: false,
|
||||
text: totalPages.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return paginationList
|
||||
},
|
||||
|
||||
// 跳转到指定页面
|
||||
goToPage(e) {
|
||||
const page = e.currentTarget.dataset.page
|
||||
if (page > 0 && page !== this.data.currentPage) {
|
||||
this.fetchEartagData(page)
|
||||
}
|
||||
},
|
||||
|
||||
// 上一页
|
||||
prevPage() {
|
||||
if (this.data.currentPage > 1) {
|
||||
this.fetchEartagData(this.data.currentPage - 1)
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
nextPage() {
|
||||
if (this.data.currentPage < this.data.totalPages) {
|
||||
this.fetchEartagData(this.data.currentPage + 1)
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化更新时间
|
||||
formatUpdateTime(timeStr) {
|
||||
if (!timeStr) return '未知时间'
|
||||
|
||||
try {
|
||||
const date = new Date(timeStr)
|
||||
if (isNaN(date.getTime())) return timeStr
|
||||
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).replace(/\//g, '-')
|
||||
} catch (error) {
|
||||
return timeStr
|
||||
}
|
||||
},
|
||||
|
||||
// 更新筛选标签计数 - 获取所有数据的总数
|
||||
async updateFilterCounts(data) {
|
||||
try {
|
||||
// 获取token
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
console.log('未找到token,无法获取统计数据')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用API获取所有数据的总数(不受分页限制)
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
limit: 10000, // 获取足够多的数据来统计
|
||||
refresh: true,
|
||||
_t: Date.now()
|
||||
},
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: 10000,
|
||||
success: (res) => {
|
||||
console.log('统计数据API调用成功:', res)
|
||||
resolve(res)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('统计数据API调用失败:', error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (response.statusCode === 200 && response.data.success) {
|
||||
const allData = response.data.data || []
|
||||
const totalCount = allData.length
|
||||
const boundCount = allData.filter(item => {
|
||||
return item.bindingStatus === '已绑定' ||
|
||||
item.isBound === true ||
|
||||
item.bound === true ||
|
||||
item.is_bound === 1 ||
|
||||
item.bandge_status === 1
|
||||
}).length
|
||||
const unboundCount = totalCount - boundCount
|
||||
|
||||
console.log('统计数据:', { totalCount, boundCount, unboundCount })
|
||||
|
||||
const filterTabs = this.data.filterTabs.map(tab => ({
|
||||
...tab,
|
||||
count: tab.type === 'all' ? totalCount :
|
||||
tab.type === 'bound' ? boundCount : unboundCount
|
||||
}))
|
||||
|
||||
this.setData({ filterTabs })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
// 如果API调用失败,使用当前数据计算
|
||||
const totalCount = data.length
|
||||
const boundCount = data.filter(item => {
|
||||
return item.bindingStatus === '已绑定' ||
|
||||
item.isBound === true ||
|
||||
item.bound === true ||
|
||||
item.is_bound === 1 ||
|
||||
item.bandge_status === 1
|
||||
}).length
|
||||
const unboundCount = totalCount - boundCount
|
||||
|
||||
const filterTabs = this.data.filterTabs.map(tab => ({
|
||||
...tab,
|
||||
count: tab.type === 'all' ? totalCount :
|
||||
tab.type === 'bound' ? boundCount : unboundCount
|
||||
}))
|
||||
|
||||
this.setData({ filterTabs })
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
const keyword = e.detail.value
|
||||
this.setData({ searchKeyword: keyword })
|
||||
|
||||
// 防抖处理,避免频繁请求
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer)
|
||||
}
|
||||
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.performSearch(keyword)
|
||||
}, 500) // 500ms防抖
|
||||
},
|
||||
|
||||
// 搜索确认
|
||||
onSearchConfirm(e) {
|
||||
const keyword = e.detail.value
|
||||
this.setData({ searchKeyword: keyword })
|
||||
|
||||
// 清除防抖定时器,立即执行搜索
|
||||
if (this.searchTimer) {
|
||||
clearTimeout(this.searchTimer)
|
||||
}
|
||||
|
||||
this.performSearch(keyword)
|
||||
},
|
||||
|
||||
// 执行搜索 - 按耳标编号精确查找
|
||||
async performSearch(keyword) {
|
||||
if (!keyword || keyword.trim() === '') {
|
||||
// 如果搜索关键词为空,重新加载第一页数据
|
||||
this.fetchEartagData(1)
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 获取token
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用私有API获取所有数据,然后进行客户端搜索
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/farm/api/smart-devices/eartags',
|
||||
method: 'GET',
|
||||
data: {
|
||||
page: 1,
|
||||
limit: 10000, // 获取所有数据进行搜索
|
||||
refresh: true,
|
||||
_t: Date.now()
|
||||
},
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
timeout: 10000,
|
||||
success: (res) => {
|
||||
console.log('搜索API调用成功:', res)
|
||||
resolve(res)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('搜索API调用失败:', error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (response.statusCode === 200 && response.data.success) {
|
||||
const allData = response.data.data || []
|
||||
|
||||
// 按耳标编号精确查找
|
||||
const searchResults = allData.filter(item => {
|
||||
const cid = item.cid ? item.cid.toString() : ''
|
||||
const eartagNumber = item.eartagNumber ? item.eartagNumber.toString() : ''
|
||||
const sn = item.sn ? item.sn.toString() : ''
|
||||
const id = item.id ? item.id.toString() : ''
|
||||
|
||||
const searchKeyword = keyword.trim().toString()
|
||||
|
||||
return cid === searchKeyword ||
|
||||
eartagNumber === searchKeyword ||
|
||||
sn === searchKeyword ||
|
||||
id === searchKeyword ||
|
||||
cid.includes(searchKeyword) ||
|
||||
eartagNumber.includes(searchKeyword) ||
|
||||
sn.includes(searchKeyword)
|
||||
})
|
||||
|
||||
console.log('搜索结果:', searchResults)
|
||||
|
||||
// 处理搜索结果
|
||||
const processedData = this.processApiData(searchResults)
|
||||
|
||||
// 计算分页信息
|
||||
const totalCount = searchResults.length
|
||||
const totalPages = Math.ceil(totalCount / this.data.pageSize)
|
||||
const paginationList = this.generatePaginationList(1, totalPages)
|
||||
|
||||
this.setData({
|
||||
originalData: response,
|
||||
allEartagList: processedData,
|
||||
eartagList: processedData,
|
||||
totalCount: totalCount,
|
||||
totalPages: totalPages,
|
||||
paginationList: paginationList,
|
||||
currentPage: 1
|
||||
})
|
||||
|
||||
// 更新筛选标签计数(基于搜索结果)
|
||||
this.updateFilterCounts(searchResults)
|
||||
|
||||
if (processedData.length === 0) {
|
||||
wx.showToast({
|
||||
title: '未找到匹配的耳标',
|
||||
icon: 'none'
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: `找到 ${processedData.length} 个匹配的耳标`,
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.data.message || '搜索失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '搜索失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 清空搜索
|
||||
clearSearch() {
|
||||
this.setData({ searchKeyword: '' })
|
||||
// 清空搜索后重新加载第一页数据
|
||||
this.fetchEartagData(1)
|
||||
},
|
||||
|
||||
// 切换筛选
|
||||
switchFilter(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
|
||||
const filterTabs = this.data.filterTabs.map(tab => ({
|
||||
...tab,
|
||||
active: tab.type === type
|
||||
}))
|
||||
|
||||
this.setData({
|
||||
currentFilter: type,
|
||||
filterTabs
|
||||
})
|
||||
|
||||
this.filterEartagList()
|
||||
},
|
||||
|
||||
// 筛选耳标列表
|
||||
filterEartagList() {
|
||||
let filteredList = [...this.data.allEartagList]
|
||||
|
||||
// 按绑定状态筛选
|
||||
if (this.data.currentFilter === 'bound') {
|
||||
filteredList = filteredList.filter(item => item.isBound)
|
||||
} else if (this.data.currentFilter === 'unbound') {
|
||||
filteredList = filteredList.filter(item => !item.isBound)
|
||||
}
|
||||
|
||||
// 按搜索关键词筛选
|
||||
if (this.data.searchKeyword.trim()) {
|
||||
const keyword = this.data.searchKeyword.toLowerCase()
|
||||
filteredList = filteredList.filter(item =>
|
||||
item.eartagNumber.toLowerCase().includes(keyword) ||
|
||||
item.hostNumber.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
this.setData({ eartagList: filteredList })
|
||||
},
|
||||
|
||||
// 点击耳标项
|
||||
onEartagClick(e) {
|
||||
const item = e.currentTarget.dataset.item
|
||||
console.log('点击耳标:', item)
|
||||
|
||||
if (!item || !item.eartagNumber) {
|
||||
wx.showToast({
|
||||
title: '数据错误',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 跳转到耳标详情页
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/eartag-detail/eartag-detail?id=${item.eartagNumber}`,
|
||||
success: () => {
|
||||
console.log('跳转成功')
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('跳转失败:', error)
|
||||
wx.showToast({
|
||||
title: '跳转失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 添加耳标
|
||||
onAddEartag() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/device/eartag-add/eartag-add'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,183 @@
|
||||
<!--pages/device/fence/fence-new.wxml-->
|
||||
<view class="fence-container">
|
||||
<!-- 顶部标题栏 -->
|
||||
<view class="header">
|
||||
<view class="header-left" bindtap="goBack">
|
||||
<text class="back-icon">‹</text>
|
||||
</view>
|
||||
<view class="header-title">电子围栏</view>
|
||||
<view class="header-right">
|
||||
<text class="menu-icon">⋯</text>
|
||||
<text class="settings-icon">⚙</text>
|
||||
<text class="location-icon">⊙</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 地图容器 -->
|
||||
<view class="map-container">
|
||||
<!-- 显示牧场按钮 -->
|
||||
<view class="show-farm-btn">
|
||||
<text>显示牧场</text>
|
||||
</view>
|
||||
|
||||
<!-- 地图组件 -->
|
||||
<map
|
||||
id="fenceMap"
|
||||
class="fence-map"
|
||||
longitude="{{mapCenter.lng}}"
|
||||
latitude="{{mapCenter.lat}}"
|
||||
scale="{{mapScale}}"
|
||||
markers="{{markers}}"
|
||||
polygons="{{polygons}}"
|
||||
bindtap="onMapTap"
|
||||
bindmarkertap="onMarkerTap"
|
||||
bindpolygontap="onPolygonTap"
|
||||
show-location="{{true}}"
|
||||
enable-3D="{{false}}"
|
||||
enable-overlooking="{{false}}"
|
||||
enable-zoom="{{true}}"
|
||||
enable-scroll="{{true}}"
|
||||
enable-rotate="{{false}}"
|
||||
enable-satellite="{{false}}"
|
||||
enable-traffic="{{false}}"
|
||||
>
|
||||
<!-- 地图控件 -->
|
||||
<cover-view class="map-controls">
|
||||
<!-- 设备统计 -->
|
||||
<cover-view class="device-stats">
|
||||
<cover-view class="stat-item">
|
||||
<cover-view class="stat-label">智能采集器:</cover-view>
|
||||
<cover-view class="stat-value">{{deviceStats.collectors}}</cover-view>
|
||||
</cover-view>
|
||||
<cover-view class="stat-item">
|
||||
<cover-view class="stat-label">智能设备:</cover-view>
|
||||
<cover-view class="stat-value">{{deviceStats.devices}}</cover-view>
|
||||
</cover-view>
|
||||
</cover-view>
|
||||
|
||||
<!-- 切换地图按钮 -->
|
||||
<cover-view class="switch-map-btn" bindtap="switchMapType">
|
||||
<cover-view class="switch-text">切换地图</cover-view>
|
||||
</cover-view>
|
||||
</cover-view>
|
||||
|
||||
<!-- 地图上的位置标记 -->
|
||||
<cover-view class="location-markers">
|
||||
<cover-view
|
||||
wx:for="{{fenceList}}"
|
||||
wx:key="id"
|
||||
class="location-marker"
|
||||
style="left: {{item.position.x}}px; top: {{item.position.y}}px;"
|
||||
>
|
||||
<cover-view class="marker-icon">📍</cover-view>
|
||||
<cover-view class="marker-label">{{item.name}}</cover-view>
|
||||
</cover-view>
|
||||
</cover-view>
|
||||
</map>
|
||||
|
||||
<!-- 底部信息栏 -->
|
||||
<view class="bottom-info">
|
||||
<view class="location-info">
|
||||
<text class="location-name">{{currentLocation.name || '各德'}}</text>
|
||||
</view>
|
||||
<view class="coordinates-info">
|
||||
<text class="coordinates">设备:{{deviceStats.total || '24065000912'}}更多>></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作面板 -->
|
||||
<view class="operation-panel" wx:if="{{showOperationPanel}}">
|
||||
<view class="panel-header">
|
||||
<text class="panel-title">{{isCreatingFence ? '新建围栏' : '围栏详情'}}</text>
|
||||
<text class="panel-close" bindtap="closeOperationPanel">×</text>
|
||||
</view>
|
||||
|
||||
<view class="panel-content">
|
||||
<view wx:if="{{isCreatingFence}}" class="create-fence-form">
|
||||
<view class="form-item">
|
||||
<text class="form-label">围栏名称:</text>
|
||||
<input class="form-input" placeholder="请输入围栏名称" value="{{newFence.name}}" bindinput="onFenceNameInput"/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">围栏类型:</text>
|
||||
<picker class="form-picker" range="{{fenceTypes}}" value="{{newFence.typeIndex}}" bindchange="onFenceTypeChange">
|
||||
<view class="picker-text">{{fenceTypes[newFence.typeIndex] || '请选择类型'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">描述:</text>
|
||||
<textarea class="form-textarea" placeholder="请输入围栏描述" value="{{newFence.description}}" bindinput="onFenceDescInput"/>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">坐标点 ({{newFence.coordinates.length}}):</text>
|
||||
<view class="coordinates-list">
|
||||
<view wx:for="{{newFence.coordinates}}" wx:key="index" class="coordinate-item">
|
||||
<text>点{{index + 1}}: {{item.lng}}, {{item.lat}}</text>
|
||||
<text class="remove-point" bindtap="removeCoordinate" data-index="{{index}}">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-actions">
|
||||
<button class="btn-cancel" bindtap="cancelCreateFence">取消</button>
|
||||
<button class="btn-save" bindtap="saveFence" disabled="{{!canSaveFence}}">保存围栏</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="fence-detail">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">围栏名称:</text>
|
||||
<text class="detail-value">{{selectedFence.name}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">围栏类型:</text>
|
||||
<text class="detail-value">{{selectedFence.type}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">面积:</text>
|
||||
<text class="detail-value">{{selectedFence.area}}平方米</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">放牧状态:</text>
|
||||
<text class="detail-value">{{selectedFence.grazingStatus}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">围栏内数量:</text>
|
||||
<text class="detail-value">{{selectedFence.insideCount}}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">围栏外数量:</text>
|
||||
<text class="detail-value">{{selectedFence.outsideCount}}</text>
|
||||
</view>
|
||||
<view class="detail-actions">
|
||||
<button class="btn-edit" bindtap="editFence">编辑</button>
|
||||
<button class="btn-delete" bindtap="deleteFence">删除</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 浮动操作按钮 -->
|
||||
<view class="fab-container">
|
||||
<view class="fab-main" bindtap="toggleFab">
|
||||
<text class="fab-icon">{{fabExpanded ? '×' : '+'}}</text>
|
||||
</view>
|
||||
<view class="fab-menu" wx:if="{{fabExpanded}}">
|
||||
<view class="fab-item" bindtap="startCreateFence">
|
||||
<text class="fab-item-icon">📍</text>
|
||||
<text class="fab-item-text">新建围栏</text>
|
||||
</view>
|
||||
<view class="fab-item" bindtap="refreshFences">
|
||||
<text class="fab-item-icon">🔄</text>
|
||||
<text class="fab-item-text">刷新数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view class="loading-overlay" wx:if="{{loading}}">
|
||||
<view class="loading-content">
|
||||
<text class="loading-text">{{loadingText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,473 @@
|
||||
/* pages/device/fence/fence-new.wxss */
|
||||
|
||||
.fence-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #7CB342, #8BC34A);
|
||||
color: white;
|
||||
padding: 0 32rpx;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.menu-icon, .settings-icon, .location-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
/* 地图容器 */
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 88rpx);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.show-farm-btn {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 20rpx;
|
||||
background: #7CB342;
|
||||
color: white;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 28rpx;
|
||||
z-index: 10;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.fence-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 地图控件 */
|
||||
.map-controls {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 20rpx;
|
||||
right: 20rpx;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.device-stats {
|
||||
position: absolute;
|
||||
top: 80rpx;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.switch-map-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: #7CB342;
|
||||
color: white;
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 28rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 位置标记 */
|
||||
.location-markers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.location-marker {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.marker-icon {
|
||||
font-size: 32rpx;
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.marker-label {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 20rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* 底部信息栏 */
|
||||
.bottom-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 1rpx solid #e8e8e8;
|
||||
}
|
||||
|
||||
.location-info {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.location-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.coordinates-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.coordinates {
|
||||
font-size: 24rpx;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 操作面板 */
|
||||
.operation-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
z-index: 200;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.panel-close {
|
||||
font-size: 48rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
/* 创建围栏表单 */
|
||||
.create-fence-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-input, .form-textarea {
|
||||
padding: 24rpx;
|
||||
border: 1rpx solid #d9d9d9;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
min-height: 120rpx;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.form-picker {
|
||||
padding: 24rpx;
|
||||
border: 1rpx solid #d9d9d9;
|
||||
border-radius: 12rpx;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.coordinates-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
max-height: 200rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.coordinate-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.remove-point {
|
||||
color: #f5222d;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.btn-cancel, .btn-save {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: #7CB342;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-save:disabled {
|
||||
background: #d9d9d9;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 围栏详情 */
|
||||
.fence-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.btn-edit, .btn-delete {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #f5222d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 浮动操作按钮 */
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
bottom: 120rpx;
|
||||
right: 32rpx;
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
.fab-main {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
background: #7CB342;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(124, 179, 66, 0.4);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 48rpx;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fab-menu {
|
||||
position: absolute;
|
||||
bottom: 140rpx;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.fab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
background: white;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fab-item-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.fab-item-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 加载提示 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: white;
|
||||
padding: 48rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 750rpx) {
|
||||
.header {
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
height: calc(100vh - 80rpx);
|
||||
}
|
||||
|
||||
.fab-container {
|
||||
bottom: 100rpx;
|
||||
right: 24rpx;
|
||||
}
|
||||
}
|
||||
@@ -1,842 +0,0 @@
|
||||
Page({
|
||||
data: {
|
||||
// 围栏数据
|
||||
fenceList: [],
|
||||
loading: false,
|
||||
|
||||
// 设备统计
|
||||
stats: {
|
||||
smartCollector: 0,
|
||||
smartDevice: 0
|
||||
},
|
||||
|
||||
// 地图相关
|
||||
mapCenter: {
|
||||
lng: 106.2751866,
|
||||
lat: 38.4689544
|
||||
},
|
||||
mapZoom: 15,
|
||||
mapLocked: true, // 地图位置锁定
|
||||
lastMapCenter: null, // 上次地图中心位置
|
||||
|
||||
// 当前选中的围栏
|
||||
selectedFence: null,
|
||||
selectedFenceIndex: 0, // 当前选中的围栏索引
|
||||
|
||||
// 显示控制
|
||||
showPasture: true,
|
||||
mapType: 'normal', // normal, satellite
|
||||
|
||||
// 地图标记和多边形
|
||||
fenceMarkers: [],
|
||||
fencePolygons: [],
|
||||
|
||||
// 缓存数据
|
||||
cachedFenceData: null,
|
||||
isOfflineMode: false,
|
||||
|
||||
// 地图锁定相关
|
||||
includePoints: [], // 用于强制锁定地图位置的点
|
||||
mapLockTimer: null, // 地图锁定监控定时器
|
||||
|
||||
// 围栏类型配置
|
||||
fenceTypes: {
|
||||
'grazing': { name: '放牧围栏', color: '#52c41a', icon: '🌿' },
|
||||
'safety': { name: '安全围栏', color: '#1890ff', icon: '🛡️' },
|
||||
'restricted': { name: '限制围栏', color: '#ff4d4f', icon: '⚠️' },
|
||||
'collector': { name: '收集围栏', color: '#fa8c16', icon: '📦' }
|
||||
},
|
||||
|
||||
// 搜索和过滤
|
||||
searchValue: '',
|
||||
selectedFenceType: '',
|
||||
filteredFenceList: []
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
console.log('电子围栏页面加载')
|
||||
this.checkLoginStatus()
|
||||
|
||||
// 启动地图锁定监控定时器
|
||||
this.startMapLockTimer()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
// 清除定时器
|
||||
if (this.data.mapLockTimer) {
|
||||
clearInterval(this.data.mapLockTimer)
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 先尝试加载缓存数据,如果没有缓存再请求API
|
||||
if (!this.loadCachedData()) {
|
||||
this.loadFenceData()
|
||||
}
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (!token || !userInfo) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
// 加载围栏数据
|
||||
loadFenceData() {
|
||||
if (!this.checkLoginStatus()) return
|
||||
|
||||
const token = wx.getStorageSync('token')
|
||||
const url = `https://ad.ningmuyun.com/farm/api/electronic-fence?page=1&limit=100&_t=${Date.now()}`
|
||||
|
||||
this.setData({ loading: true })
|
||||
wx.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: 30000,
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('围栏API响应:', res)
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const response = res.data
|
||||
|
||||
if (response.success && response.data) {
|
||||
const fenceList = response.data.map(fence => this.formatFenceData(fence))
|
||||
|
||||
// 生成地图标记和多边形数据
|
||||
const fenceMarkers = this.generateFenceMarkers(fenceList)
|
||||
const fencePolygons = this.generateFencePolygons(fenceList)
|
||||
|
||||
// 缓存数据
|
||||
const cacheData = {
|
||||
fenceList: fenceList,
|
||||
fenceMarkers: fenceMarkers,
|
||||
fencePolygons: fencePolygons,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
wx.setStorageSync('fenceCache', cacheData)
|
||||
|
||||
this.setData({
|
||||
fenceList: fenceList,
|
||||
fenceMarkers: fenceMarkers,
|
||||
fencePolygons: fencePolygons,
|
||||
cachedFenceData: cacheData,
|
||||
isOfflineMode: false,
|
||||
stats: {
|
||||
smartCollector: 2, // 从API获取或硬编码
|
||||
smartDevice: 4 // 从API获取或硬编码
|
||||
}
|
||||
})
|
||||
|
||||
// 如果有围栏数据,设置默认选中第一个围栏
|
||||
if (fenceList.length > 0) {
|
||||
const firstFence = fenceList[0]
|
||||
const centerLng = parseFloat(firstFence.center.lng)
|
||||
const centerLat = parseFloat(firstFence.center.lat)
|
||||
|
||||
this.setData({
|
||||
selectedFence: firstFence,
|
||||
selectedFenceIndex: 0,
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
},
|
||||
mapLocked: true, // 初始化后锁定地图
|
||||
lastMapCenter: {
|
||||
latitude: centerLat,
|
||||
longitude: centerLng
|
||||
}
|
||||
})
|
||||
|
||||
// 立即更新include-points来强制锁定
|
||||
this.updateIncludePoints()
|
||||
|
||||
// 多次强制锁定,确保地图不会移动
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('第一次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('第二次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 1000)
|
||||
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('第三次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 2000)
|
||||
}
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} else if (res.statusCode === 502) {
|
||||
wx.showModal({
|
||||
title: '服务器错误',
|
||||
content: '服务器暂时不可用(502错误),请稍后重试',
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
setTimeout(() => {
|
||||
this.loadFenceData()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (res.statusCode >= 500) {
|
||||
wx.showModal({
|
||||
title: '服务器错误',
|
||||
content: `服务器错误(${res.statusCode}),请稍后重试`,
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
setTimeout(() => {
|
||||
this.loadFenceData()
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: res.data?.message || `请求失败(${res.statusCode})`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('请求失败:', err)
|
||||
|
||||
// 根据错误类型显示不同的提示和处理方式
|
||||
let errorMessage = '网络请求失败'
|
||||
let errorTitle = '请求失败'
|
||||
let showRetry = true
|
||||
|
||||
if (err.errMsg && err.errMsg.includes('timeout')) {
|
||||
errorMessage = '请求超时,服务器响应较慢'
|
||||
errorTitle = '请求超时'
|
||||
} else if (err.errMsg && err.errMsg.includes('fail')) {
|
||||
if (err.errMsg.includes('502')) {
|
||||
errorMessage = '服务器网关错误(502),服务暂时不可用'
|
||||
errorTitle = '服务器错误'
|
||||
} else if (err.errMsg.includes('503')) {
|
||||
errorMessage = '服务器维护中(503),请稍后重试'
|
||||
errorTitle = '服务维护'
|
||||
} else if (err.errMsg.includes('504')) {
|
||||
errorMessage = '服务器响应超时(504),请重试'
|
||||
errorTitle = '服务器超时'
|
||||
} else {
|
||||
errorMessage = '网络连接失败,请检查网络设置'
|
||||
errorTitle = '网络错误'
|
||||
}
|
||||
} else if (err.errMsg && err.errMsg.includes('ssl')) {
|
||||
errorMessage = 'SSL证书错误,请检查网络环境'
|
||||
errorTitle = '安全连接错误'
|
||||
} else if (err.errMsg && err.errMsg.includes('dns')) {
|
||||
errorMessage = 'DNS解析失败,请检查网络连接'
|
||||
errorTitle = '网络解析错误'
|
||||
}
|
||||
|
||||
// 尝试加载缓存数据
|
||||
this.loadCachedData()
|
||||
|
||||
if (showRetry) {
|
||||
wx.showModal({
|
||||
title: errorTitle,
|
||||
content: errorMessage + ',是否重试?',
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 显示重试提示
|
||||
wx.showLoading({
|
||||
title: '重试中...',
|
||||
mask: true
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.hideLoading()
|
||||
this.loadFenceData()
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: errorMessage,
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化围栏数据
|
||||
formatFenceData(fence) {
|
||||
// 处理围栏类型映射
|
||||
let fenceType = fence.type || 'grazing'
|
||||
if (fenceType === '放牧围栏') fenceType = 'grazing'
|
||||
else if (fenceType === '安全围栏') fenceType = 'safety'
|
||||
else if (fenceType === '限制围栏') fenceType = 'restricted'
|
||||
else if (fenceType === '收集围栏') fenceType = 'collector'
|
||||
|
||||
return {
|
||||
id: fence.id,
|
||||
name: fence.name,
|
||||
type: fenceType,
|
||||
typeName: this.data.fenceTypes[fenceType]?.name || fenceType,
|
||||
typeColor: this.data.fenceTypes[fenceType]?.color || '#666',
|
||||
typeIcon: this.data.fenceTypes[fenceType]?.icon || '📍',
|
||||
description: fence.description,
|
||||
coordinates: fence.coordinates || [],
|
||||
center: fence.center,
|
||||
area: fence.area,
|
||||
grazingStatus: fence.grazingStatus,
|
||||
insideCount: fence.insideCount,
|
||||
outsideCount: fence.outsideCount,
|
||||
isActive: fence.isActive,
|
||||
createdAt: fence.createdAt,
|
||||
updatedAt: fence.updatedAt
|
||||
}
|
||||
},
|
||||
|
||||
// 加载缓存数据
|
||||
loadCachedData() {
|
||||
try {
|
||||
const cachedData = wx.getStorageSync('fenceCache')
|
||||
if (cachedData && cachedData.timestamp) {
|
||||
const cacheAge = Date.now() - cachedData.timestamp
|
||||
const maxCacheAge = 24 * 60 * 60 * 1000 // 24小时
|
||||
|
||||
if (cacheAge < maxCacheAge) {
|
||||
console.log('加载缓存数据,缓存时间:', new Date(cachedData.timestamp))
|
||||
|
||||
this.setData({
|
||||
fenceList: cachedData.fenceList || [],
|
||||
fenceMarkers: cachedData.fenceMarkers || [],
|
||||
fencePolygons: cachedData.fencePolygons || [],
|
||||
isOfflineMode: true,
|
||||
stats: {
|
||||
smartCollector: 2,
|
||||
smartDevice: 4
|
||||
}
|
||||
})
|
||||
|
||||
// 如果有围栏数据,设置默认选中第一个围栏
|
||||
if (cachedData.fenceList && cachedData.fenceList.length > 0) {
|
||||
const firstFence = cachedData.fenceList[0]
|
||||
const centerLng = parseFloat(firstFence.center.lng)
|
||||
const centerLat = parseFloat(firstFence.center.lat)
|
||||
|
||||
this.setData({
|
||||
selectedFence: firstFence,
|
||||
selectedFenceIndex: 0,
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
},
|
||||
mapLocked: true,
|
||||
lastMapCenter: {
|
||||
latitude: centerLat,
|
||||
longitude: centerLng
|
||||
}
|
||||
})
|
||||
|
||||
// 立即更新include-points来强制锁定
|
||||
this.updateIncludePoints()
|
||||
|
||||
// 多次强制锁定,确保地图不会移动
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('缓存数据第一次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
}
|
||||
})
|
||||
this.updateIncludePoints()
|
||||
console.log('缓存数据第二次延迟强制锁定:', centerLng, centerLat)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 显示离线模式提示
|
||||
wx.showToast({
|
||||
title: '已加载缓存数据(离线模式)',
|
||||
icon: 'none',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
return true
|
||||
} else {
|
||||
console.log('缓存数据已过期,清除缓存')
|
||||
wx.removeStorageSync('fenceCache')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载缓存数据失败:', error)
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
// 返回上一页
|
||||
onBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
// 显示设置菜单
|
||||
onShowMenu() {
|
||||
const menuItems = ['围栏设置', '设备管理', '历史记录']
|
||||
|
||||
// 添加地图锁定/解锁选项
|
||||
const lockText = this.data.mapLocked ? '解锁地图' : '锁定地图'
|
||||
menuItems.unshift(lockText)
|
||||
|
||||
// 如果有多个围栏,添加围栏切换选项
|
||||
if (this.data.fenceList.length > 1) {
|
||||
menuItems.unshift('切换围栏')
|
||||
}
|
||||
|
||||
wx.showActionSheet({
|
||||
itemList: menuItems,
|
||||
success: (res) => {
|
||||
const tapIndex = res.tapIndex
|
||||
console.log('选择了:', tapIndex)
|
||||
|
||||
let actualIndex = tapIndex
|
||||
|
||||
// 处理围栏切换
|
||||
if (this.data.fenceList.length > 1 && tapIndex === 0) {
|
||||
this.showFenceSelector()
|
||||
return
|
||||
} else if (this.data.fenceList.length > 1) {
|
||||
actualIndex = tapIndex - 1
|
||||
}
|
||||
|
||||
// 处理地图锁定/解锁
|
||||
if (actualIndex === 0) {
|
||||
this.toggleMapLock()
|
||||
} else {
|
||||
// 其他菜单项
|
||||
const menuIndex = this.data.fenceList.length > 1 ? actualIndex - 1 : actualIndex
|
||||
switch (menuIndex) {
|
||||
case 0: // 围栏设置
|
||||
wx.showToast({ title: '围栏设置功能开发中', icon: 'none' })
|
||||
break
|
||||
case 1: // 设备管理
|
||||
wx.showToast({ title: '设备管理功能开发中', icon: 'none' })
|
||||
break
|
||||
case 2: // 历史记录
|
||||
wx.showToast({ title: '历史记录功能开发中', icon: 'none' })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 显示围栏选择器
|
||||
showFenceSelector() {
|
||||
const fenceNames = this.data.fenceList.map(fence => fence.name)
|
||||
|
||||
wx.showActionSheet({
|
||||
itemList: fenceNames,
|
||||
success: (res) => {
|
||||
const selectedIndex = res.tapIndex
|
||||
const selectedFence = this.data.fenceList[selectedIndex]
|
||||
|
||||
this.setData({
|
||||
selectedFence: selectedFence,
|
||||
selectedFenceIndex: selectedIndex
|
||||
})
|
||||
|
||||
// 自动定位到选中的围栏
|
||||
this.locateToSelectedFence()
|
||||
|
||||
wx.showToast({
|
||||
title: `已切换到${selectedFence.name}`,
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换牧场显示
|
||||
onTogglePasture() {
|
||||
const newShowPasture = !this.data.showPasture
|
||||
this.setData({
|
||||
showPasture: newShowPasture
|
||||
})
|
||||
|
||||
if (newShowPasture && this.data.selectedFence) {
|
||||
// 如果显示牧场,定位到选中的围栏
|
||||
this.locateToSelectedFence()
|
||||
}
|
||||
},
|
||||
|
||||
// 定位到选中的围栏
|
||||
locateToSelectedFence() {
|
||||
if (!this.data.selectedFence) {
|
||||
wx.showToast({
|
||||
title: '没有选中的围栏',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const fence = this.data.selectedFence
|
||||
|
||||
// 计算围栏的边界,用于设置合适的地图视野
|
||||
const coordinates = fence.coordinates
|
||||
if (coordinates && coordinates.length > 0) {
|
||||
let minLat = coordinates[0].lat
|
||||
let maxLat = coordinates[0].lat
|
||||
let minLng = coordinates[0].lng
|
||||
let maxLng = coordinates[0].lng
|
||||
|
||||
coordinates.forEach(coord => {
|
||||
minLat = Math.min(minLat, coord.lat)
|
||||
maxLat = Math.max(maxLat, coord.lat)
|
||||
minLng = Math.min(minLng, coord.lng)
|
||||
maxLng = Math.max(maxLng, coord.lng)
|
||||
})
|
||||
|
||||
// 计算中心点和合适的缩放级别
|
||||
const centerLat = (minLat + maxLat) / 2
|
||||
const centerLng = (minLng + maxLng) / 2
|
||||
|
||||
// 根据围栏大小调整缩放级别
|
||||
const latDiff = maxLat - minLat
|
||||
const lngDiff = maxLng - minLng
|
||||
const maxDiff = Math.max(latDiff, lngDiff)
|
||||
|
||||
let zoom = 15
|
||||
if (maxDiff > 0.01) zoom = 12
|
||||
else if (maxDiff > 0.005) zoom = 14
|
||||
else if (maxDiff > 0.002) zoom = 16
|
||||
else zoom = 18
|
||||
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: centerLng,
|
||||
lat: centerLat
|
||||
},
|
||||
mapZoom: zoom,
|
||||
mapLocked: true, // 定位后锁定地图
|
||||
lastMapCenter: {
|
||||
latitude: centerLat,
|
||||
longitude: centerLng
|
||||
}
|
||||
})
|
||||
|
||||
// 立即更新include-points来强制锁定
|
||||
setTimeout(() => {
|
||||
this.updateIncludePoints()
|
||||
}, 100)
|
||||
|
||||
wx.showToast({
|
||||
title: `已定位到${fence.name}`,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
} else {
|
||||
// 如果没有坐标点,使用中心点定位
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: parseFloat(fence.center.lng),
|
||||
lat: parseFloat(fence.center.lat)
|
||||
},
|
||||
mapZoom: 15,
|
||||
mapLocked: true, // 定位后锁定地图
|
||||
lastMapCenter: {
|
||||
latitude: parseFloat(fence.center.lat),
|
||||
longitude: parseFloat(fence.center.lng)
|
||||
}
|
||||
})
|
||||
|
||||
// 立即更新include-points来强制锁定
|
||||
setTimeout(() => {
|
||||
this.updateIncludePoints()
|
||||
}, 100)
|
||||
|
||||
wx.showToast({
|
||||
title: `已定位到${fence.name}`,
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 切换地图类型
|
||||
onSwitchMap() {
|
||||
const mapType = this.data.mapType === 'normal' ? 'satellite' : 'normal'
|
||||
this.setData({
|
||||
mapType: mapType
|
||||
})
|
||||
|
||||
wx.showToast({
|
||||
title: mapType === 'normal' ? '切换到普通地图' : '切换到卫星地图',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 解锁/锁定地图
|
||||
toggleMapLock() {
|
||||
const newLocked = !this.data.mapLocked
|
||||
this.setData({
|
||||
mapLocked: newLocked
|
||||
})
|
||||
|
||||
// 更新include-points
|
||||
this.updateIncludePoints()
|
||||
|
||||
wx.showToast({
|
||||
title: newLocked ? '地图已锁定' : '地图已解锁',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
},
|
||||
|
||||
// 地图标记点击事件
|
||||
onMarkerTap(e) {
|
||||
const markerId = e.detail.markerId
|
||||
const fence = this.data.fenceList.find(f => f.id === markerId)
|
||||
|
||||
if (fence) {
|
||||
// 选中该围栏
|
||||
this.setData({
|
||||
selectedFence: fence,
|
||||
selectedFenceIndex: this.data.fenceList.findIndex(f => f.id === markerId)
|
||||
})
|
||||
|
||||
wx.showModal({
|
||||
title: `${fence.typeIcon} ${fence.name}`,
|
||||
content: `类型: ${fence.typeName}\n状态: ${fence.grazingStatus}\n面积: ${fence.area}平方米\n坐标点: ${fence.coordinates.length}个\n描述: ${fence.description || '无描述'}`,
|
||||
confirmText: '定位到此围栏',
|
||||
cancelText: '关闭',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 定位到选中的围栏
|
||||
this.locateToSelectedFence()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 地图区域变化
|
||||
onRegionChange(e) {
|
||||
console.log('地图区域变化:', e.detail)
|
||||
|
||||
// 强制锁定地图 - 无论什么情况都恢复位置
|
||||
if (this.data.lastMapCenter) {
|
||||
console.log('强制锁定地图位置')
|
||||
|
||||
// 立即恢复地图位置
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: this.data.lastMapCenter.longitude,
|
||||
lat: this.data.lastMapCenter.latitude
|
||||
}
|
||||
})
|
||||
|
||||
// 更新include-points来强制锁定
|
||||
this.updateIncludePoints()
|
||||
}
|
||||
},
|
||||
|
||||
// 更新include-points来强制锁定地图
|
||||
updateIncludePoints() {
|
||||
if (this.data.lastMapCenter) {
|
||||
const center = this.data.lastMapCenter
|
||||
|
||||
// 创建更紧密的四个点来强制锁定地图视野
|
||||
const offset = 0.0005 // 减小偏移量,使锁定更紧密
|
||||
const points = [
|
||||
{ latitude: center.latitude - offset, longitude: center.longitude - offset },
|
||||
{ latitude: center.latitude + offset, longitude: center.longitude - offset },
|
||||
{ latitude: center.latitude + offset, longitude: center.longitude + offset },
|
||||
{ latitude: center.latitude - offset, longitude: center.longitude + offset }
|
||||
]
|
||||
|
||||
this.setData({
|
||||
includePoints: points
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
includePoints: []
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 启动地图锁定监控定时器
|
||||
startMapLockTimer() {
|
||||
if (this.data.mapLockTimer) {
|
||||
clearInterval(this.data.mapLockTimer)
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (this.data.lastMapCenter) {
|
||||
// 强制更新地图位置 - 无论锁定状态如何
|
||||
this.setData({
|
||||
mapCenter: {
|
||||
lng: this.data.lastMapCenter.longitude,
|
||||
lat: this.data.lastMapCenter.latitude
|
||||
}
|
||||
})
|
||||
|
||||
// 更新include-points
|
||||
this.updateIncludePoints()
|
||||
|
||||
console.log('定时器强制锁定地图位置:', this.data.lastMapCenter)
|
||||
}
|
||||
}, 500) // 每500毫秒检查一次,更频繁
|
||||
|
||||
this.setData({
|
||||
mapLockTimer: timer
|
||||
})
|
||||
},
|
||||
|
||||
// 地图点击事件
|
||||
onMapTap(e) {
|
||||
console.log('地图点击:', e.detail)
|
||||
},
|
||||
|
||||
// 关闭围栏信息面板
|
||||
onCloseFenceInfo() {
|
||||
this.setData({
|
||||
selectedFence: null
|
||||
})
|
||||
},
|
||||
|
||||
// 定位围栏
|
||||
onLocateFence() {
|
||||
if (this.data.selectedFence) {
|
||||
this.locateToSelectedFence()
|
||||
}
|
||||
},
|
||||
|
||||
// 查看围栏详情
|
||||
onViewFenceDetails() {
|
||||
if (this.data.selectedFence) {
|
||||
wx.showModal({
|
||||
title: `${this.data.selectedFence.typeIcon} ${this.data.selectedFence.name}`,
|
||||
content: `围栏ID: ${this.data.selectedFence.id}\n类型: ${this.data.selectedFence.typeName}\n状态: ${this.data.selectedFence.grazingStatus}\n面积: ${this.data.selectedFence.area}平方米\n坐标点: ${this.data.selectedFence.coordinates.length}个\n内部设备: ${this.data.selectedFence.insideCount}个\n外部设备: ${this.data.selectedFence.outsideCount}个\n创建时间: ${this.data.selectedFence.createdAt}\n更新时间: ${this.data.selectedFence.updatedAt}\n描述: ${this.data.selectedFence.description || '无描述'}`,
|
||||
showCancel: false,
|
||||
confirmText: '确定'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 生成围栏标记
|
||||
generateFenceMarkers(fenceList) {
|
||||
return fenceList.map((fence, index) => {
|
||||
return {
|
||||
id: fence.id,
|
||||
latitude: parseFloat(fence.center.lat),
|
||||
longitude: parseFloat(fence.center.lng),
|
||||
iconPath: '', // 使用默认图标
|
||||
width: 30,
|
||||
height: 30,
|
||||
title: fence.name,
|
||||
callout: {
|
||||
content: `${fence.typeIcon} ${fence.name}\n${fence.typeName}\n${fence.grazingStatus}\n${fence.coordinates.length}个坐标点`,
|
||||
color: '#333',
|
||||
fontSize: 12,
|
||||
borderRadius: 8,
|
||||
bgColor: '#fff',
|
||||
padding: 12,
|
||||
display: 'BYCLICK',
|
||||
borderWidth: 1,
|
||||
borderColor: fence.typeColor || '#3cc51f'
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 生成围栏多边形
|
||||
generateFencePolygons(fenceList) {
|
||||
return fenceList.map((fence, index) => {
|
||||
const points = fence.coordinates.map(coord => ({
|
||||
latitude: coord.lat,
|
||||
longitude: coord.lng
|
||||
}))
|
||||
|
||||
// 根据围栏类型设置颜色
|
||||
const strokeColor = fence.typeColor || (fence.isActive ? '#3cc51f' : '#ff6b6b')
|
||||
const fillColor = fence.typeColor ?
|
||||
`${fence.typeColor}33` : // 添加透明度
|
||||
(fence.isActive ? 'rgba(60, 197, 31, 0.2)' : 'rgba(255, 107, 107, 0.2)')
|
||||
|
||||
return {
|
||||
points: points,
|
||||
strokeWidth: 3,
|
||||
strokeColor: strokeColor,
|
||||
fillColor: fillColor
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "电子围栏",
|
||||
"navigationBarBackgroundColor": "#3cc51f",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#f5f5f5"
|
||||
}
|
||||
@@ -1,148 +1,2 @@
|
||||
<!-- 电子围栏页面 -->
|
||||
<view class="fence-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="header">
|
||||
<view class="header-left" bindtap="onBack">
|
||||
<text class="back-icon">‹</text>
|
||||
</view>
|
||||
<view class="header-title">电子围栏</view>
|
||||
<view class="header-right">
|
||||
<text class="menu-icon" bindtap="onShowMenu">⋯</text>
|
||||
<text class="minimize-icon" bindtap="onShowMenu">−</text>
|
||||
<text class="target-icon" bindtap="onShowMenu">◎</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 离线模式提示 -->
|
||||
<view wx:if="{{isOfflineMode}}" class="offline-notice">
|
||||
<text class="offline-icon">📡</text>
|
||||
<text class="offline-text">离线模式 - 显示缓存数据</text>
|
||||
</view>
|
||||
|
||||
<!-- 地图锁定提示 -->
|
||||
<view wx:if="{{mapLocked}}" class="map-lock-notice">
|
||||
<text class="lock-icon">🔒</text>
|
||||
<text class="lock-text">地图已锁定 - 防止自动移动</text>
|
||||
</view>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<view class="control-panel">
|
||||
<!-- 左侧控制区 -->
|
||||
<view class="left-controls">
|
||||
<!-- 设置按钮 -->
|
||||
<view class="settings-btn" bindtap="onShowMenu">
|
||||
<text class="settings-icon">⚙</text>
|
||||
</view>
|
||||
|
||||
<!-- 显示牧场按钮 -->
|
||||
<view class="pasture-btn {{showPasture ? 'active' : ''}}" bindtap="onTogglePasture">
|
||||
<text>显示牧场</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备统计信息 -->
|
||||
<view class="device-stats">
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">智能采集器:</text>
|
||||
<text class="stats-value">{{stats.smartCollector}}</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">智能设备:</text>
|
||||
<text class="stats-value">{{stats.smartDevice}}</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">围栏总数:</text>
|
||||
<text class="stats-value">{{fenceList.length}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 牧场名称 -->
|
||||
<view class="pasture-name">各德</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧控制区 -->
|
||||
<view class="right-controls">
|
||||
<view class="switch-map-btn" bindtap="onSwitchMap">
|
||||
<text>切换地图</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 围栏信息面板 -->
|
||||
<view wx:if="{{selectedFence}}" class="fence-info-panel">
|
||||
<view class="panel-header">
|
||||
<view class="fence-title">
|
||||
<text class="fence-icon">{{selectedFence.typeIcon}}</text>
|
||||
<text class="fence-name">{{selectedFence.name}}</text>
|
||||
</view>
|
||||
<view class="close-btn" bindtap="onCloseFenceInfo">
|
||||
<text>✕</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-content">
|
||||
<view class="info-row">
|
||||
<text class="info-label">围栏类型:</text>
|
||||
<text class="info-value" style="color: {{selectedFence.typeColor}}">{{selectedFence.typeName}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">放牧状态:</text>
|
||||
<text class="info-value">{{selectedFence.grazingStatus}}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">围栏面积:</text>
|
||||
<text class="info-value">{{selectedFence.area}}平方米</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">坐标点数:</text>
|
||||
<text class="info-value">{{selectedFence.coordinates.length}}个</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">围栏描述:</text>
|
||||
<text class="info-value">{{selectedFence.description || '无描述'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-actions">
|
||||
<view class="action-btn primary" bindtap="onLocateFence">
|
||||
<text>定位围栏</text>
|
||||
</view>
|
||||
<view class="action-btn secondary" bindtap="onViewFenceDetails">
|
||||
<text>查看详情</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 地图区域 -->
|
||||
<view class="map-container">
|
||||
<map
|
||||
id="fenceMap"
|
||||
class="fence-map"
|
||||
longitude="{{mapCenter.lng}}"
|
||||
latitude="{{mapCenter.lat}}"
|
||||
scale="{{mapZoom}}"
|
||||
markers="{{fenceMarkers}}"
|
||||
polygons="{{fencePolygons}}"
|
||||
show-location="{{false}}"
|
||||
enable-scroll="{{false}}"
|
||||
enable-zoom="{{false}}"
|
||||
enable-rotate="{{false}}"
|
||||
enable-overlooking="{{false}}"
|
||||
enable-satellite="{{false}}"
|
||||
enable-traffic="{{false}}"
|
||||
enable-3D="{{false}}"
|
||||
enable-compass="{{false}}"
|
||||
enable-scale="{{false}}"
|
||||
enable-poi="{{false}}"
|
||||
enable-building="{{false}}"
|
||||
include-points="{{includePoints}}"
|
||||
bindmarkertap="onMarkerTap"
|
||||
bindregionchange="onRegionChange"
|
||||
bindtap="onMapTap"
|
||||
>
|
||||
<!-- 地图加载中 -->
|
||||
<view wx:if="{{loading}}" class="map-loading">
|
||||
<text>地图加载中...</text>
|
||||
</view>
|
||||
</map>
|
||||
</view>
|
||||
</view>
|
||||
<!--pages/device/fence/fence.wxml-->
|
||||
<text>pages/device/fence/fence.wxml</text>
|
||||
@@ -1,358 +0,0 @@
|
||||
/* 电子围栏页面样式 */
|
||||
|
||||
.fence-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 离线模式提示 */
|
||||
.offline-notice {
|
||||
width: 100%;
|
||||
background: #ff9500;
|
||||
color: #ffffff;
|
||||
padding: 16rpx 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
font-size: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.offline-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.offline-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 地图锁定提示 */
|
||||
.map-lock-notice {
|
||||
width: 100%;
|
||||
background: #007aff;
|
||||
color: #ffffff;
|
||||
padding: 16rpx 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
font-size: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.lock-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 顶部导航栏 */
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #3cc51f;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 36rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.menu-icon,
|
||||
.minimize-icon,
|
||||
.target-icon {
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 控制面板 */
|
||||
.control-panel {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 32rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 左侧控制区 */
|
||||
.left-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pasture-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #3cc51f;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
.pasture-btn.active {
|
||||
background: #2a9d16;
|
||||
}
|
||||
|
||||
.device-stats {
|
||||
background: #333333;
|
||||
border-radius: 8rpx;
|
||||
padding: 24rpx;
|
||||
min-width: 240rpx;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stats-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pasture-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 右侧控制区 */
|
||||
.right-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.switch-map-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
background: #3cc51f;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
/* 地图容器 */
|
||||
.map-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.fence-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #ffffff;
|
||||
padding: 24rpx 48rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* 围栏信息面板 */
|
||||
.fence-info-panel {
|
||||
position: absolute;
|
||||
top: 200rpx;
|
||||
right: 32rpx;
|
||||
width: 320rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
background: linear-gradient(135deg, #3cc51f, #2a9d16);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.fence-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.fence-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.fence-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 12rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #3cc51f;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #ffffff;
|
||||
color: #3cc51f;
|
||||
border: 2rpx solid #3cc51f;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 750rpx) {
|
||||
.control-panel {
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.right-controls {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.device-stats {
|
||||
min-width: 200rpx;
|
||||
}
|
||||
|
||||
.fence-info-panel {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
width: 100%;
|
||||
margin: 16rpx 0;
|
||||
}
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
Page({
|
||||
data: {
|
||||
list: [],
|
||||
searchValue: '',
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
totalPages: 0,
|
||||
pageNumbers: [],
|
||||
paginationList: [], // 分页页码列表
|
||||
stats: {
|
||||
total: 0,
|
||||
online: 0,
|
||||
offline: 0
|
||||
},
|
||||
loading: false,
|
||||
isSearching: false,
|
||||
searchResult: null
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('智能主机页面加载')
|
||||
this.checkLoginStatus()
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadData().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (!token || !userInfo) {
|
||||
console.log('用户未登录,跳转到登录页')
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再使用',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('用户已登录:', userInfo.username)
|
||||
return true
|
||||
},
|
||||
|
||||
// 加载数据
|
||||
loadData() {
|
||||
const { currentPage, pageSize } = this.data
|
||||
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) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
wx.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: 30000, // 设置30秒超时
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('API响应:', res)
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const response = res.data
|
||||
|
||||
if (response.success && response.data) {
|
||||
const data = response.data
|
||||
const total = response.total || 0
|
||||
const totalPages = Math.ceil(total / this.data.pageSize)
|
||||
const paginationList = this.generatePaginationList(this.data.currentPage, totalPages)
|
||||
|
||||
this.setData({
|
||||
list: Array.isArray(data) ? data.map(item => this.formatItemData(item)) : [],
|
||||
total: total,
|
||||
totalPages: totalPages,
|
||||
paginationList: paginationList,
|
||||
stats: response.stats || { total: 0, online: 0, offline: 0 }
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: res.data?.message || '数据加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('请求失败:', err)
|
||||
|
||||
// 根据错误类型显示不同的提示
|
||||
let errorMessage = '网络请求失败'
|
||||
if (err.errMsg && err.errMsg.includes('timeout')) {
|
||||
errorMessage = '请求超时,请检查网络连接'
|
||||
} else if (err.errMsg && err.errMsg.includes('fail')) {
|
||||
errorMessage = '网络连接失败,请重试'
|
||||
} else if (err.errMsg && err.errMsg.includes('401')) {
|
||||
errorMessage = '请登录后重试'
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: '请求失败',
|
||||
content: errorMessage + ',是否重试?',
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户选择重试
|
||||
setTimeout(() => {
|
||||
this.loadData()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 格式化单个设备数据
|
||||
formatItemData(item) {
|
||||
return {
|
||||
...item,
|
||||
statusText: item.networkStatus || '未知',
|
||||
signalText: item.signalValue || '未知',
|
||||
batteryText: `${item.battery || 0}%`,
|
||||
temperatureText: `${item.temperature || 0}°C`,
|
||||
deviceNumberText: item.deviceNumber || '未知',
|
||||
updateTimeText: item.updateTime || '未知'
|
||||
}
|
||||
},
|
||||
|
||||
// 生成分页页码列表
|
||||
generatePaginationList(currentPage, totalPages) {
|
||||
const paginationList = []
|
||||
const maxVisiblePages = 5 // 最多显示5个页码
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
// 总页数少于等于5页,显示所有页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 总页数大于5页,显示省略号
|
||||
if (currentPage <= 3) {
|
||||
// 当前页在前3页
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
paginationList.push({
|
||||
page: totalPages,
|
||||
active: false,
|
||||
text: totalPages.toString()
|
||||
})
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
// 当前页在后3页
|
||||
paginationList.push({
|
||||
page: 1,
|
||||
active: false,
|
||||
text: '1'
|
||||
})
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
for (let i = totalPages - 3; i <= totalPages; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 当前页在中间
|
||||
paginationList.push({
|
||||
page: 1,
|
||||
active: false,
|
||||
text: '1'
|
||||
})
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
paginationList.push({
|
||||
page: i,
|
||||
active: i === currentPage,
|
||||
text: i.toString()
|
||||
})
|
||||
}
|
||||
paginationList.push({
|
||||
page: -1,
|
||||
active: false,
|
||||
text: '...'
|
||||
})
|
||||
paginationList.push({
|
||||
page: totalPages,
|
||||
active: false,
|
||||
text: totalPages.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return paginationList
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchValue: e.detail.value.trim() })
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
onSearch() {
|
||||
const searchValue = this.data.searchValue.trim()
|
||||
if (!searchValue) {
|
||||
wx.showToast({
|
||||
title: '请输入主机编号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证主机编号格式(数字和字母)
|
||||
if (!/^[A-Za-z0-9]+$/.test(searchValue)) {
|
||||
wx.showToast({
|
||||
title: '主机编号只能包含数字和字母',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置搜索状态
|
||||
this.setData({
|
||||
isSearching: true,
|
||||
searchResult: null,
|
||||
currentPage: 1
|
||||
})
|
||||
|
||||
// 执行精确搜索
|
||||
this.performExactSearch(searchValue)
|
||||
},
|
||||
|
||||
// 执行精确搜索
|
||||
performExactSearch(searchValue) {
|
||||
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) {
|
||||
wx.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '搜索中...' })
|
||||
wx.request({
|
||||
url,
|
||||
method: 'GET',
|
||||
timeout: 30000, // 设置30秒超时
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('搜索API响应:', res)
|
||||
if (res.statusCode === 200 && res.data) {
|
||||
const response = res.data
|
||||
|
||||
if (response.success && response.data) {
|
||||
const data = response.data
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
// 找到匹配的设备
|
||||
const device = this.formatItemData(data[0])
|
||||
this.setData({
|
||||
searchResult: device,
|
||||
list: [], // 清空列表显示
|
||||
total: 1,
|
||||
totalPages: 1,
|
||||
paginationList: [{ page: 1, active: true, text: '1' }]
|
||||
})
|
||||
wx.showToast({
|
||||
title: '搜索成功',
|
||||
icon: 'success'
|
||||
})
|
||||
} else {
|
||||
// 没有找到匹配的设备
|
||||
this.setData({
|
||||
searchResult: null,
|
||||
list: [],
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
paginationList: []
|
||||
})
|
||||
wx.showToast({
|
||||
title: '未找到该设备',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '搜索失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} else if (res.statusCode === 401) {
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
})
|
||||
wx.removeStorageSync('token')
|
||||
wx.removeStorageSync('userInfo')
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '搜索失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('搜索请求失败:', err)
|
||||
|
||||
// 根据错误类型显示不同的提示
|
||||
let errorMessage = '网络请求失败'
|
||||
if (err.errMsg && err.errMsg.includes('timeout')) {
|
||||
errorMessage = '搜索超时,请检查网络连接'
|
||||
} else if (err.errMsg && err.errMsg.includes('fail')) {
|
||||
errorMessage = '网络连接失败,请重试'
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: '搜索失败',
|
||||
content: errorMessage + ',是否重试?',
|
||||
confirmText: '重试',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 用户选择重试
|
||||
setTimeout(() => {
|
||||
this.performExactSearch(this.data.searchValue)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
wx.hideLoading()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 清除搜索
|
||||
clearSearch() {
|
||||
this.setData({
|
||||
searchValue: '',
|
||||
isSearching: false,
|
||||
searchResult: null,
|
||||
currentPage: 1
|
||||
})
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
// 上一页
|
||||
onPrevPage() {
|
||||
if (this.data.currentPage > 1) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage - 1
|
||||
}, () => {
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
onNextPage() {
|
||||
if (this.data.currentPage < this.data.totalPages) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage + 1
|
||||
}, () => {
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 分页切换
|
||||
onPageChange(e) {
|
||||
const page = parseInt(e.currentTarget.dataset.page)
|
||||
if (page > 0 && page !== this.data.currentPage) {
|
||||
this.setData({
|
||||
currentPage: page
|
||||
}, () => {
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 查看主机详情
|
||||
viewHostDetail(e) {
|
||||
const hostId = e.currentTarget.dataset.id
|
||||
wx.navigateTo({
|
||||
url: `/pages/device/host-detail/host-detail?id=${hostId}`
|
||||
})
|
||||
},
|
||||
|
||||
// 主机定位总览
|
||||
onLocationOverview() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/device/host-location/host-location'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "智能主机",
|
||||
"enablePullDownRefresh": true,
|
||||
"onReachBottomDistance": 50
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
// pages/home/home.js
|
||||
const { get } = require('../../utils/api')
|
||||
const { alertApi } = require('../../services/api.js')
|
||||
const { formatTime } = require('../../utils/index')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
// 预警标签页
|
||||
alertTabs: [
|
||||
{ name: '项圈预警', active: true },
|
||||
{ name: '耳标预警', active: false },
|
||||
{ name: '脚环预警', active: false },
|
||||
{ name: '主机预警', active: false }
|
||||
],
|
||||
// 当前预警数据(项圈预警)
|
||||
currentAlertData: [
|
||||
{ title: '今日未被采集', value: '6', isAlert: false, bgIcon: '📄', url: '/pages/alert/collar' },
|
||||
{ title: '项圈绑带剪断', value: '0', isAlert: false, bgIcon: '🏢', url: '/pages/alert/collar' },
|
||||
{ title: '电子围栏', value: '3', isAlert: false, bgIcon: '🚧', url: '/pages/device/fence/fence' },
|
||||
{ title: '今日运动量偏高', value: '0', isAlert: false, bgIcon: '📈', url: '/pages/alert/collar' },
|
||||
{ title: '今日运动量偏低', value: '3', isAlert: true, bgIcon: '📉', url: '/pages/alert/collar' },
|
||||
{ title: '传输频次过快', value: '0', isAlert: false, bgIcon: '⚡', url: '/pages/alert/collar' },
|
||||
{ title: '电量偏低', value: '2', isAlert: false, bgIcon: '🔋', url: '/pages/alert/collar' }
|
||||
],
|
||||
// 智能设备
|
||||
smartDevices: [
|
||||
{ name: '智能项圈', icon: 'A', color: 'orange', url: '/pages/device/collar/collar' },
|
||||
{ name: '智能耳标', icon: '👂', color: 'blue', url: '/pages/device/eartag/eartag' },
|
||||
{ name: '智能脚环', icon: '📎', color: 'purple', url: '/pages/device/ankle/ankle' },
|
||||
{ name: '智能主机', icon: '🖥️', color: 'blue', url: '/pages/device/host/host' },
|
||||
{ name: '视频监控', icon: '📹', color: 'orange', url: '/pages/monitor/monitor' },
|
||||
{ name: '环境监测', icon: '🌡️', color: 'blue', url: '/pages/environment/environment' }
|
||||
],
|
||||
// 智能工具
|
||||
smartTools: [
|
||||
{ name: '电子围栏', icon: '🎯', color: 'orange', url: '/pages/device/fence/fence' },
|
||||
{ name: '扫码溯源', icon: '🛡️', color: 'blue', url: '/pages/trace' },
|
||||
{ name: '档案拍照', icon: '📷', color: 'red', url: '/pages/photo' },
|
||||
{ name: '检测工具', icon: '📊', color: 'purple', url: '/pages/detection' }
|
||||
],
|
||||
// 业务办理
|
||||
businessOps: [
|
||||
{ name: '电子检疫', icon: '📋', color: 'orange', url: '/pages/quarantine' },
|
||||
{ name: '电子确权', icon: '👤', color: 'blue', url: '/pages/rights' },
|
||||
{ name: '无害化处理申报', icon: '♻️', color: 'purple', url: '/pages/disposal' }
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
this.fetchHomeData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.fetchHomeData()
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.fetchHomeData().then(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
const token = wx.getStorageSync('token')
|
||||
const userInfo = wx.getStorageSync('userInfo')
|
||||
|
||||
if (!token || !userInfo) {
|
||||
console.log('用户未登录,跳转到登录页')
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再使用',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('用户已登录:', userInfo.username)
|
||||
return true
|
||||
},
|
||||
|
||||
// 获取首页数据
|
||||
async fetchHomeData() {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 这里可以调用API获取真实数据
|
||||
// const alertData = await get('/alert/data')
|
||||
// const deviceData = await get('/device/data')
|
||||
|
||||
// 暂时使用模拟数据
|
||||
console.log('首页数据加载完成')
|
||||
} catch (error) {
|
||||
console.error('获取首页数据失败:', error)
|
||||
wx.showToast({
|
||||
title: '获取数据失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换预警标签页
|
||||
switchAlertTab(e) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
const alertTabs = this.data.alertTabs.map((tab, i) => ({
|
||||
...tab,
|
||||
active: i === index
|
||||
}))
|
||||
|
||||
this.setData({ alertTabs })
|
||||
|
||||
// 根据选中的标签页更新预警数据
|
||||
this.updateAlertData(index)
|
||||
},
|
||||
|
||||
// 更新预警数据
|
||||
async updateAlertData(tabIndex) {
|
||||
let alertData = []
|
||||
|
||||
switch (tabIndex) {
|
||||
case 0: // 项圈预警
|
||||
alertData = [
|
||||
{ title: '今日未被采集', value: '6', isAlert: false, bgIcon: '📄', url: '/pages/alert/collar' },
|
||||
{ title: '项圈绑带剪断', value: '0', isAlert: false, bgIcon: '🏢', url: '/pages/alert/collar' },
|
||||
{ title: '电子围栏', value: '3', isAlert: false, bgIcon: '🚧', url: '/pages/device/fence/fence' },
|
||||
{ title: '今日运动量偏高', value: '0', isAlert: false, bgIcon: '📈', url: '/pages/alert/collar' },
|
||||
{ title: '今日运动量偏低', value: '3', isAlert: true, bgIcon: '📉', url: '/pages/alert/collar' },
|
||||
{ title: '传输频次过快', value: '0', isAlert: false, bgIcon: '⚡', url: '/pages/alert/collar' },
|
||||
{ title: '电量偏低', value: '2', isAlert: false, bgIcon: '🔋', url: '/pages/alert/collar' }
|
||||
]
|
||||
break
|
||||
case 1: // 耳标预警
|
||||
// 动态调用真实耳标预警接口,使用返回数据填充首页卡片
|
||||
await this.fetchEartagAlertCards()
|
||||
return
|
||||
case 2: // 脚环预警
|
||||
alertData = [
|
||||
{ title: '今日未被采集', value: '1', isAlert: false, bgIcon: '📄', url: '/pages/alert/ankle' },
|
||||
{ title: '脚环松动', value: '2', isAlert: true, bgIcon: '📎', url: '/pages/alert/ankle' },
|
||||
{ title: '运动异常', value: '0', isAlert: false, bgIcon: '🏃', url: '/pages/alert/ankle' },
|
||||
{ title: '电量偏低', value: '1', isAlert: false, bgIcon: '🔋', url: '/pages/alert/ankle' }
|
||||
]
|
||||
break
|
||||
case 3: // 主机预警
|
||||
alertData = [
|
||||
{ title: '网络连接异常', value: '1', isAlert: true, bgIcon: '📶', url: '/pages/alert/host' },
|
||||
{ title: '存储空间不足', value: '0', isAlert: false, bgIcon: '💾', url: '/pages/alert/host' },
|
||||
{ title: '温度过高', value: '0', isAlert: false, bgIcon: '🌡️', url: '/pages/alert/host' },
|
||||
{ title: '电量偏低', value: '0', isAlert: false, bgIcon: '🔋', url: '/pages/alert/host' }
|
||||
]
|
||||
break
|
||||
}
|
||||
|
||||
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
|
||||
console.log('首页导航到:', url)
|
||||
|
||||
if (url) {
|
||||
wx.navigateTo({
|
||||
url,
|
||||
success: () => {
|
||||
console.log('导航成功:', url)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('导航失败:', error)
|
||||
wx.showToast({
|
||||
title: '页面不存在',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '链接错误',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,229 +0,0 @@
|
||||
// pages/login/login.js
|
||||
const { post } = require('../../utils/api')
|
||||
const auth = require('../../utils/auth')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
formData: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
loading: false,
|
||||
agreedToTerms: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
console.log('登录页面加载')
|
||||
|
||||
// 检查是否已经登录
|
||||
if (auth.isLoggedIn()) {
|
||||
wx.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
return
|
||||
}
|
||||
},
|
||||
|
||||
// 输入框变化
|
||||
onInputChange(e) {
|
||||
const { field } = e.currentTarget.dataset
|
||||
const { value } = e.detail
|
||||
|
||||
this.setData({
|
||||
[`formData.${field}`]: value
|
||||
})
|
||||
},
|
||||
|
||||
// 切换用户协议状态
|
||||
toggleAgreement() {
|
||||
console.log('点击用户协议,当前状态:', this.data.agreedToTerms)
|
||||
const newState = !this.data.agreedToTerms
|
||||
console.log('切换后状态:', newState)
|
||||
this.setData({
|
||||
agreedToTerms: newState
|
||||
})
|
||||
},
|
||||
|
||||
// 处理登录
|
||||
async handleLogin() {
|
||||
const { username, password } = this.data.formData
|
||||
const { agreedToTerms } = this.data
|
||||
|
||||
// 验证输入
|
||||
if (!username.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入账号',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
wx.showToast({
|
||||
title: '请输入密码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!agreedToTerms) {
|
||||
wx.showToast({
|
||||
title: '请同意用户协议和隐私政策',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 调用真实的登录API
|
||||
const response = await this.realLogin(username.trim(), password.trim())
|
||||
console.log('登录API响应:', response)
|
||||
|
||||
if (response.success) {
|
||||
console.log('登录成功,token:', response.token)
|
||||
// 保存登录信息
|
||||
auth.login(response.token, {
|
||||
id: response.user?.id || 1,
|
||||
username: response.user?.username || username.trim(),
|
||||
nickname: '管理员',
|
||||
avatar: '',
|
||||
role: response.role?.name || 'admin'
|
||||
})
|
||||
console.log('用户信息已保存')
|
||||
|
||||
wx.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
wx.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: response.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
wx.showToast({
|
||||
title: error.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 真实登录API
|
||||
async realLogin(username, password) {
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: 'https://ad.ningmuyun.com/farm/api/auth/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
username: username,
|
||||
password: password
|
||||
},
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout: 10000,
|
||||
success: (res) => {
|
||||
console.log('登录API调用成功:', res)
|
||||
resolve(res)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.log('登录API调用失败:', error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error('登录请求失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录API调用异常:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
// 模拟登录API(保留作为备用)
|
||||
mockLogin(username, password) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
// 模拟登录验证
|
||||
if (username === 'admin' && password === '123456') {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
token: 'mock_token_' + Date.now(),
|
||||
userInfo: {
|
||||
id: 1,
|
||||
username: username,
|
||||
nickname: '管理员',
|
||||
avatar: '',
|
||||
role: 'admin'
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
message: '账号或密码错误'
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
|
||||
// 一键登录
|
||||
onOneClickLogin() {
|
||||
wx.showToast({
|
||||
title: '一键登录功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 短信登录
|
||||
onSmsLogin() {
|
||||
wx.showToast({
|
||||
title: '短信登录功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 注册账号
|
||||
onRegister() {
|
||||
wx.showToast({
|
||||
title: '注册功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 其他登录方式
|
||||
onOtherLogin() {
|
||||
wx.showToast({
|
||||
title: '其他登录方式开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 语言选择
|
||||
onLanguageSelect() {
|
||||
wx.showToast({
|
||||
title: '语言切换功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,181 +0,0 @@
|
||||
// pages/production/production.js
|
||||
Page({
|
||||
data: {
|
||||
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('生产管理页面加载')
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('生产管理页面显示')
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
// 下拉刷新
|
||||
setTimeout(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
}, 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
|
||||
console.log('导航到:', url)
|
||||
|
||||
if (url) {
|
||||
// 检查页面是否存在
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
|
||||
// 如果页面不存在,显示提示
|
||||
wx.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
// 注释掉实际跳转,等页面创建后再启用
|
||||
// wx.navigateTo({
|
||||
// url,
|
||||
// fail: (error) => {
|
||||
// console.error('页面跳转失败:', error)
|
||||
// wx.showToast({
|
||||
// title: '页面不存在',
|
||||
// icon: 'none'
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
}
|
||||
},
|
||||
|
||||
// 处理功能点击
|
||||
onFunctionClick(e) {
|
||||
const functionType = e.currentTarget.dataset.type
|
||||
const animalType = e.currentTarget.dataset.animal
|
||||
|
||||
console.log('点击功能:', animalType, functionType)
|
||||
|
||||
// 根据功能类型显示不同的提示
|
||||
const functionNames = {
|
||||
'archive': '档案管理',
|
||||
'estrus-record': '发情记录',
|
||||
'mating-record': '配种记录',
|
||||
'pregnancy-check': '妊检记录',
|
||||
'farrowing-record': '分娩记录',
|
||||
'weaning-record': '断奶记录',
|
||||
'pen-transfer': '转栏记录',
|
||||
'pen-exit': '离栏记录',
|
||||
'pen-settings': '栏舍设置',
|
||||
'batch-settings': '批次设置',
|
||||
'epidemic-warning': '防疫预警',
|
||||
'scan-entry': '扫码录入',
|
||||
'scan-print': '扫码打印'
|
||||
}
|
||||
|
||||
const animalNames = {
|
||||
'cattle': '牛',
|
||||
'pig': '猪',
|
||||
'sheep': '羊',
|
||||
'poultry': '家禽'
|
||||
}
|
||||
|
||||
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',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,245 +0,0 @@
|
||||
// pages/profile/profile.js
|
||||
const auth = require('../../utils/auth')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
userInfo: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
nickname: '管理员',
|
||||
realName: '张三',
|
||||
userId: '15586823774',
|
||||
avatar: '',
|
||||
role: 'admin',
|
||||
department: '技术部',
|
||||
cattleCount: 156,
|
||||
deviceCount: 89,
|
||||
alertCount: 12,
|
||||
farmCount: 3,
|
||||
appVersion: '1.0.0'
|
||||
},
|
||||
menuItems: [
|
||||
{
|
||||
icon: '⚙️',
|
||||
title: '养殖系统设置',
|
||||
url: '/pages/profile/system-settings/system-settings'
|
||||
},
|
||||
{
|
||||
icon: '🔄',
|
||||
title: '切换养殖场',
|
||||
url: '/pages/profile/switch-farm/switch-farm'
|
||||
},
|
||||
{
|
||||
icon: '🏷️',
|
||||
title: '养殖场识别码',
|
||||
url: '/pages/profile/farm-code/farm-code'
|
||||
},
|
||||
{
|
||||
icon: '📄',
|
||||
title: '关联机构',
|
||||
url: '/pages/profile/associated-institutions/associated-institutions'
|
||||
},
|
||||
{
|
||||
icon: '⭐',
|
||||
title: '首页自定义',
|
||||
url: '/pages/profile/homepage-customization/homepage-customization'
|
||||
},
|
||||
{
|
||||
icon: '🏠',
|
||||
title: '养殖场设置',
|
||||
url: '/pages/profile/farm-settings/farm-settings'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('我的页面加载')
|
||||
this.loadUserInfo()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('我的页面显示')
|
||||
this.loadUserInfo()
|
||||
},
|
||||
|
||||
// 加载用户信息
|
||||
loadUserInfo() {
|
||||
const userInfo = auth.getUserInfo()
|
||||
if (userInfo) {
|
||||
// 合并模拟数据
|
||||
const mergedUserInfo = {
|
||||
...this.data.userInfo,
|
||||
...userInfo
|
||||
}
|
||||
this.setData({ userInfo: mergedUserInfo })
|
||||
} else {
|
||||
// 如果未登录,跳转到登录页
|
||||
auth.redirectToLogin()
|
||||
}
|
||||
},
|
||||
|
||||
// 点击菜单项
|
||||
onMenuTap(e) {
|
||||
const url = e.currentTarget.dataset.url
|
||||
console.log('点击菜单项:', url)
|
||||
|
||||
if (url) {
|
||||
wx.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
// 注释掉实际跳转,等页面创建后再启用
|
||||
// wx.navigateTo({
|
||||
// url,
|
||||
// fail: (error) => {
|
||||
// console.error('页面跳转失败:', error)
|
||||
// wx.showToast({
|
||||
// title: '页面不存在',
|
||||
// icon: 'none'
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
}
|
||||
},
|
||||
|
||||
// 编辑个人信息
|
||||
editProfile() {
|
||||
console.log('编辑个人信息')
|
||||
wx.showToast({
|
||||
title: '编辑功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
async logout() {
|
||||
const result = await wx.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
confirmText: '退出',
|
||||
confirmColor: '#f5222d'
|
||||
})
|
||||
|
||||
if (result.confirm) {
|
||||
try {
|
||||
wx.showLoading({ title: '退出中...' })
|
||||
|
||||
// 清除本地存储
|
||||
auth.logout()
|
||||
|
||||
wx.showToast({
|
||||
title: '已退出登录',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
wx.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
wx.showToast({
|
||||
title: '退出失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 清除缓存
|
||||
async clearCache() {
|
||||
const result = await wx.showModal({
|
||||
title: '清除缓存',
|
||||
content: '确定要清除应用缓存吗?',
|
||||
confirmText: '清除',
|
||||
confirmColor: '#faad14'
|
||||
})
|
||||
|
||||
if (result.confirm) {
|
||||
try {
|
||||
wx.showLoading({ title: '清除中...' })
|
||||
|
||||
// 清除微信小程序缓存
|
||||
wx.clearStorageSync()
|
||||
|
||||
wx.showToast({
|
||||
title: '缓存已清除',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新加载用户信息
|
||||
this.loadUserInfo()
|
||||
} catch (error) {
|
||||
console.error('清除缓存失败:', error)
|
||||
wx.showToast({
|
||||
title: '清除失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 检查更新
|
||||
async checkUpdate() {
|
||||
try {
|
||||
wx.showLoading({ title: '检查中...' })
|
||||
|
||||
// 模拟检查更新
|
||||
setTimeout(() => {
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '检查更新',
|
||||
content: '当前已是最新版本',
|
||||
showCancel: false,
|
||||
confirmText: '确定'
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
wx.showToast({
|
||||
title: '检查失败',
|
||||
icon: 'none'
|
||||
})
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
// 获取用户显示名称
|
||||
getUserDisplayName() {
|
||||
const userInfo = this.data.userInfo
|
||||
return userInfo.realName || userInfo.nickname || userInfo.username || '未知用户'
|
||||
},
|
||||
|
||||
// 获取用户头像
|
||||
getUserAvatar() {
|
||||
const userInfo = this.data.userInfo
|
||||
return userInfo.avatar || '/images/default-avatar.png'
|
||||
},
|
||||
|
||||
// 获取用户角色
|
||||
getUserRole() {
|
||||
const userInfo = this.data.userInfo
|
||||
const roleMap = {
|
||||
'admin': '管理员',
|
||||
'manager': '经理',
|
||||
'operator': '操作员',
|
||||
'viewer': '观察员'
|
||||
}
|
||||
return roleMap[userInfo.role] || '普通用户'
|
||||
},
|
||||
|
||||
// 获取用户部门
|
||||
getUserDepartment() {
|
||||
const userInfo = this.data.userInfo
|
||||
return userInfo.department || '未知部门'
|
||||
}
|
||||
})
|
||||
@@ -40,7 +40,7 @@
|
||||
"setting": {
|
||||
"bundle": false,
|
||||
"userConfirmedBundleSwitch": false,
|
||||
"urlCheck": true,
|
||||
"urlCheck": false,
|
||||
"scopeDataCheck": false,
|
||||
"coverView": true,
|
||||
"es6": true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user