修改bug。新增牛只,超链接

This commit is contained in:
xuqiuyun
2025-09-15 18:18:41 +08:00
parent be32363412
commit 89bc6e8ce2
18 changed files with 2183 additions and 76 deletions

View File

@@ -23,7 +23,7 @@
宁夏智慧养殖监管平台
</div>
<div class="user-info">
<a href="https://nxzhyz.xiyumuge.com/" target="_blank" style="color: white; text-decoration: none; margin-right: 16px;">大屏展示</a>
<a href="https://datav.ningmuyun.com/" target="_blank" style="color: white; text-decoration: none; margin-right: 16px;">大屏展示</a>
<span>欢迎, {{ userData?.username }}</span>
<a-button type="link" @click="handleLogout" style="color: white;">退出</a-button>
</div>

View File

@@ -21,27 +21,15 @@
<template v-for="subMenu in menu.children">
<!-- 子菜单还有子菜单 -->
<a-sub-menu v-if="subMenu.children && subMenu.children.length > 0" :key="subMenu.id">
<template #icon>
<component :is="subMenu.icon" v-if="subMenu.icon" />
<SettingOutlined v-else />
</template>
<template #title>{{ subMenu.name }}</template>
<a-menu-item v-for="item in subMenu.children" :key="item.id">
<template #icon>
<component :is="item.icon" v-if="item.icon" />
<FileOutlined v-else />
</template>
{{ item.name }}
</a-menu-item>
</a-sub-menu>
<!-- 子菜单是叶子节点 -->
<a-menu-item v-else :key="subMenu.id">
<template #icon>
<component :is="subMenu.icon" v-if="subMenu.icon" />
<FileOutlined v-else />
</template>
{{ subMenu.name }}
</a-menu-item>
</template>
@@ -60,7 +48,7 @@
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { ref, onMounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
HomeOutlined,
@@ -101,6 +89,11 @@ const selectedKeys = ref([])
const openKeys = ref([])
const menuTree = ref([])
// 监听selectedKeys变化用于调试
watch(selectedKeys, (newKeys) => {
console.log('selectedKeys变化:', newKeys)
}, { deep: true })
// 图标映射
const iconMap = {
'DashboardOutlined': DashboardOutlined,
@@ -195,8 +188,10 @@ const loadMenus = async () => {
console.log('菜单树构建完成:', menuTree.value)
console.log('菜单树长度:', menuTree.value.length)
// 设置当前选中的菜单项
setActiveMenu()
// 设置当前选中的菜单项使用nextTick确保DOM更新完成
nextTick(() => {
setActiveMenu()
})
} catch (error) {
console.error('加载菜单数据失败:', error)
message.error('加载菜单数据失败')
@@ -273,11 +268,14 @@ const buildMenuTree = (menus) => {
const setActiveMenu = () => {
const currentPath = route.path
console.log('当前路径:', currentPath)
console.log('当前菜单树:', menuTree.value)
// 查找匹配的菜单项
const findMenuByPath = (menus, path) => {
for (const menu of menus) {
console.log('检查菜单项:', menu.name, menu.path, '匹配路径:', path)
if (menu.path === path) {
console.log('找到匹配的菜单项:', menu.name, menu.id)
return menu
}
if (menu.children) {
@@ -289,12 +287,32 @@ const setActiveMenu = () => {
}
const activeMenu = findMenuByPath(menuTree.value, currentPath)
console.log('找到的激活菜单:', activeMenu)
if (activeMenu && activeMenu.id != null) {
selectedKeys.value = [activeMenu.id.toString()]
// 保持与模板中key绑定的数据类型一致数字类型
const activeMenuId = activeMenu.id
console.log('设置激活菜单项:', activeMenuId, '当前selectedKeys:', selectedKeys.value)
// 设置打开的父菜单
// 强制更新选中状态,确保高亮显示
selectedKeys.value = [activeMenuId]
console.log('更新selectedKeys为:', selectedKeys.value)
// 强制触发响应式更新
nextTick(() => {
console.log('nextTick后selectedKeys:', selectedKeys.value)
})
// 设置打开的父菜单,但保持已有的展开状态
const parentIds = getParentMenuIds(activeMenu.id, menuTree.value)
openKeys.value = parentIds ? parentIds.map(id => id != null ? id.toString() : '').filter(id => id) : []
if (parentIds && parentIds.length > 0) {
const parentKeys = parentIds.map(id => id != null ? id.toString() : '').filter(id => id)
console.log('设置父级菜单展开:', parentKeys)
// 合并已有的展开状态和新的父级菜单
openKeys.value = [...new Set([...openKeys.value, ...parentKeys])]
}
} else {
console.log('未找到匹配的菜单项,当前路径:', currentPath)
}
}
@@ -316,6 +334,45 @@ const getParentMenuIds = (menuId, menus, parentIds = []) => {
const handleOpenChange = (keys) => {
console.log('菜单展开状态变化:', keys)
openKeys.value = keys
// 当一级菜单展开时,自动选中第一个二级菜单项
if (keys.length > 0) {
const lastOpenedKey = keys[keys.length - 1]
console.log('最后展开的菜单:', lastOpenedKey)
// 查找对应的一级菜单
const findMenuById = (menus, id) => {
for (const menu of menus) {
if (menu && menu.id != null && menu.id.toString() === id.toString()) {
return menu
}
if (menu && menu.children) {
const found = findMenuById(menu.children, id)
if (found) return found
}
}
return null
}
const openedMenu = findMenuById(menuTree.value, lastOpenedKey)
if (openedMenu && openedMenu.children && openedMenu.children.length > 0) {
// 找到第一个二级菜单项
const firstChild = openedMenu.children[0]
if (firstChild && firstChild.id != null) {
console.log('自动选中第一个二级菜单项:', firstChild.name, firstChild.id)
selectedKeys.value = [firstChild.id]
// 如果第一个子菜单还有子菜单,继续查找
if (firstChild.children && firstChild.children.length > 0) {
const firstGrandChild = firstChild.children[0]
if (firstGrandChild && firstGrandChild.id != null) {
console.log('自动选中第一个三级菜单项:', firstGrandChild.name, firstGrandChild.id)
selectedKeys.value = [firstGrandChild.id]
}
}
}
}
}
}
// 处理菜单点击
@@ -350,6 +407,24 @@ const handleMenuClick = ({ key }) => {
const menuItem = findMenuById(menuTree.value, actualKey)
if (menuItem && menuItem.path) {
console.log('导航到:', menuItem.path)
// 确保父级菜单保持展开状态
const parentIds = getParentMenuIds(menuItem.id, menuTree.value)
if (parentIds && parentIds.length > 0) {
const parentKeys = parentIds.map(id => id != null ? id.toString() : '').filter(id => id)
console.log('保持父级菜单展开:', parentKeys)
// 使用Set确保不重复并保持原有展开状态
const newOpenKeys = [...new Set([...openKeys.value, ...parentKeys])]
openKeys.value = newOpenKeys
}
// 先设置选中的菜单项,确保高亮显示
// 将字符串转换为数字类型以匹配模板中的key绑定
const menuId = parseInt(actualKey)
selectedKeys.value = [menuId]
console.log('设置选中菜单项:', menuId)
// 然后导航到对应路径
router.push(menuItem.path)
emit('menu-click', menuItem)
} else {
@@ -378,28 +453,24 @@ const getDefaultMenus = () => {
id: 21,
name: '智能耳标',
path: '/smart-devices/eartag',
icon: 'FileOutlined',
children: []
},
{
id: 22,
name: '智能项圈',
path: '/smart-devices/collar',
icon: 'FileOutlined',
children: []
},
{
id: 23,
name: '智能主机',
path: '/smart-devices/host',
icon: 'FileOutlined',
children: []
},
{
id: 24,
name: '电子围栏',
path: '/smart-devices/fence',
icon: 'FileOutlined',
children: []
}
]
@@ -414,14 +485,12 @@ const getDefaultMenus = () => {
id: 31,
name: '智能耳标预警',
path: '/smart-alerts/eartag',
icon: 'FileOutlined',
children: []
},
{
id: 32,
name: '智能项圈预警',
path: '/smart-alerts/collar',
icon: 'FileOutlined',
children: []
}
]
@@ -436,35 +505,30 @@ const getDefaultMenus = () => {
id: 41,
name: '牛只档案',
path: '/cattle-management/archives',
icon: 'FileOutlined',
children: []
},
{
id: 42,
name: '栏舍设置',
path: '/cattle-management/pens',
icon: 'FileOutlined',
children: []
},
{
id: 43,
name: '批次设置',
path: '/cattle-management/batches',
icon: 'FileOutlined',
children: []
},
{
id: 44,
name: '转栏记录',
path: '/cattle-management/transfer-records',
icon: 'FileOutlined',
children: []
},
{
id: 45,
name: '离栏记录',
path: '/cattle-management/exit-records',
icon: 'FileOutlined',
children: []
}
]
@@ -479,28 +543,24 @@ const getDefaultMenus = () => {
id: 51,
name: '养殖场信息管理',
path: '/farm-management/info',
icon: 'FileOutlined',
children: []
},
{
id: 52,
name: '栏舍管理',
path: '/farm-management/pens',
icon: 'FileOutlined',
children: []
},
{
id: 53,
name: '用户管理',
path: '/farm-management/users',
icon: 'FileOutlined',
children: []
},
{
id: 54,
name: '角色权限管理',
path: '/farm-management/role-permissions',
icon: 'SafetyOutlined',
children: []
}
]
@@ -517,7 +577,12 @@ const getDefaultMenus = () => {
// 监听路由变化
watch(() => route.path, () => {
setActiveMenu()
// 确保菜单树已加载完成后再设置激活状态
if (menuTree.value && menuTree.value.length > 0) {
nextTick(() => {
setActiveMenu()
})
}
})
// 监听用户数据变化,当用户数据加载完成后再加载菜单

View File

@@ -158,13 +158,71 @@ watch(() => route.name, (newRouteName) => {
)
if (currentRoute && !openKeys.value.includes(currentRoute.name)) {
console.log('自动展开父级菜单:', currentRoute.name)
openKeys.value = [...openKeys.value, currentRoute.name]
}
}, { immediate: true })
// 监听路由变化,确保父级菜单保持展开状态
watch(() => route.path, (newPath) => {
// 查找当前路径对应的父级菜单
const findParentRoute = (routes, targetPath) => {
for (const route of routes) {
if (route.children) {
for (const child of route.children) {
if (child.path === targetPath) {
return route
}
}
// 递归查找更深层的子路由
const found = findParentRoute(route.children, targetPath)
if (found) return found
}
}
return null
}
const parentRoute = findParentRoute(router.options.routes, newPath)
if (parentRoute && !openKeys.value.includes(parentRoute.name)) {
console.log('保持父级菜单展开:', parentRoute.name)
openKeys.value = [...openKeys.value, parentRoute.name]
}
}, { immediate: true })
// 处理子菜单展开/收起
const handleOpenChange = (keys) => {
openKeys.value = keys
// 当一级菜单展开时,自动选中第一个二级菜单项
if (keys.length > 0) {
const lastOpenedKey = keys[keys.length - 1]
console.log('最后展开的菜单:', lastOpenedKey)
// 查找对应的一级菜单
const findRouteByName = (routes, name) => {
for (const route of routes) {
if (route.name === name) {
return route
}
if (route.children) {
const found = findRouteByName(route.children, name)
if (found) return found
}
}
return null
}
const openedRoute = findRouteByName(router.options.routes, lastOpenedKey)
if (openedRoute && openedRoute.children && openedRoute.children.length > 0) {
// 找到第一个二级菜单项
const firstChild = openedRoute.children[0]
if (firstChild && firstChild.name) {
console.log('自动选中第一个二级菜单项:', firstChild.meta?.title, firstChild.name)
// 导航到第一个子菜单
router.push(firstChild.path)
}
}
}
}
// 组件挂载时设置调试工具

View File

@@ -129,32 +129,61 @@
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="品系" name="strain">
<a-input
<a-select
v-model:value="formData.strain"
placeholder="请输入品系"
@input="handleFieldChange('strain', $event.target.value)"
@change="handleFieldChange('strain', $event.target.value)"
/>
placeholder="请选择品系"
:loading="cattleUsersLoading"
@change="handleFieldChange('strain', $event)"
show-search
:filter-option="filterOption"
>
<a-select-option
v-for="user in cattleUsers"
:key="user.id"
:value="user.id"
>
{{ user.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="品种" name="varieties">
<a-input
<a-select
v-model:value="formData.varieties"
placeholder="请输入品种"
@input="handleFieldChange('varieties', $event.target.value)"
@change="handleFieldChange('varieties', $event.target.value)"
/>
placeholder="请选择品种"
:loading="cattleTypesLoading"
@change="handleFieldChange('varieties', $event)"
show-search
:filter-option="filterOption"
>
<a-select-option
v-for="type in cattleTypes"
:key="type.id"
:value="type.id"
>
{{ type.name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="类别" name="cate">
<a-input
<a-select
v-model:value="formData.cate"
placeholder="请输入类别"
@input="handleFieldChange('cate', $event.target.value)"
@change="handleFieldChange('cate', $event.target.value)"
/>
placeholder="请选择类别"
@change="handleFieldChange('cate', $event)"
show-search
:filter-option="filterCateOption"
>
<a-select-option
v-for="(name, value) in cateOptions"
:key="value"
:value="parseInt(value)"
>
{{ name }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
@@ -368,15 +397,29 @@ const animals = ref([])
const farms = ref([])
const pens = ref([])
const batches = ref([])
const cattleUsers = ref([]) // 品系数据cattle_user表
const cattleTypes = ref([]) // 品种数据cattle_type表
const loading = ref(false)
const farmsLoading = ref(false)
const pensLoading = ref(false)
const batchesLoading = ref(false)
const cattleUsersLoading = ref(false)
const cattleTypesLoading = ref(false)
const modalVisible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
const formRef = ref()
// 类别选项(预定义映射)
const cateOptions = ref({
1: '犊牛',
2: '育成母牛',
3: '架子牛',
4: '青年牛',
5: '基础母牛',
6: '育肥牛'
})
// 分页相关
const pagination = ref({
current: 1,
@@ -474,19 +517,38 @@ const columns = [
title: '品系',
dataIndex: 'strain', // 映射iot_cattle.strain显示用途名称
key: 'strain',
width: 120
width: 120,
customRender: ({ text }) => {
const user = cattleUsers.value.find(u => u.id === text)
return user ? user.name : text || '-'
}
},
{
title: '品种',
dataIndex: 'varieties', // 映射iot_cattle.varieties
key: 'varieties',
width: 120
width: 120,
customRender: ({ text }) => {
const type = cattleTypes.value.find(t => t.id === text)
return type ? type.name : text || '-'
}
},
{
title: '类别',
dataIndex: 'cate', // 映射iot_cattle.cate
key: 'cate',
width: 100
width: 100,
customRender: ({ text }) => {
const cateMap = {
1: '犊牛',
2: '育成母牛',
3: '架子牛',
4: '青年牛',
5: '基础母牛',
6: '育肥牛'
}
return cateMap[text] || text || '-'
}
},
{
title: '出生体重(kg)',
@@ -611,6 +673,38 @@ const fetchBatches = async (farmId = null) => {
}
}
// 获取品系列表cattle_user表
const fetchCattleUsers = async () => {
try {
cattleUsersLoading.value = true
const response = await api.get('/cattle-user')
if (response.success) {
cattleUsers.value = response.data
console.log('获取品系列表成功:', cattleUsers.value.length, '条记录')
}
} catch (error) {
console.error('获取品系列表失败:', error)
} finally {
cattleUsersLoading.value = false
}
}
// 获取品种和类别列表cattle_type表
const fetchCattleTypes = async () => {
try {
cattleTypesLoading.value = true
const response = await api.get('/cattle-type')
if (response.success) {
cattleTypes.value = response.data
console.log('获取品种列表成功:', cattleTypes.value.length, '条记录')
}
} catch (error) {
console.error('获取品种列表失败:', error)
} finally {
cattleTypesLoading.value = false
}
}
// 字段变化监听函数
const handleFieldChange = (fieldName, value) => {
console.log(`字段 ${fieldName} 变化:`, value)
@@ -625,6 +719,54 @@ const handleFieldChange = (fieldName, value) => {
formData[fieldName] = value
}
// 下拉框过滤选项方法
const filterOption = (input, option) => {
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
// 类别下拉框过滤选项方法
const filterCateOption = (input, option) => {
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
// 绑定耳标到iot_jbq_client表
const bindEarTag = async (earNumber, cattleId) => {
try {
console.log('开始绑定耳标:', earNumber, '到牛只ID:', cattleId)
// 查找对应的耳标设备
const response = await api.get('/iot-jbq-client', {
params: { cid: earNumber }
})
if (response.success && response.data && response.data.length > 0) {
const device = response.data[0]
console.log('找到耳标设备:', device)
// 更新设备的绑定状态
const updateResponse = await api.put(`/iot-jbq-client/${device.id}`, {
bandge_status: 1, // 1表示已绑定
bound_cattle_id: cattleId,
bound_ear_number: earNumber
})
if (updateResponse.success) {
console.log('耳标绑定成功')
message.success('耳标绑定成功')
} else {
console.error('耳标绑定失败:', updateResponse.message)
message.warning('牛只档案创建成功,但耳标绑定失败')
}
} else {
console.warn('未找到对应的耳标设备:', earNumber)
message.warning('牛只档案创建成功,但未找到对应的耳标设备')
}
} catch (error) {
console.error('耳标绑定失败:', error)
message.warning('牛只档案创建成功,但耳标绑定失败')
}
}
// 处理动物类型选择变化
const handleTypeChange = (value) => {
console.log('=== 动物类型变化 ===')
@@ -775,6 +917,8 @@ const openModal = () => {
// 加载必需数据
const loadRequiredData = () => {
fetchFarms()
fetchCattleUsers()
fetchCattleTypes()
}
// 提交表单使用iot_cattle API
@@ -789,11 +933,20 @@ const handleSubmit = async () => {
console.log('是否编辑模式:', isEdit.value);
console.log('原始表单数据:', JSON.parse(JSON.stringify(formData)));
// 处理日期格式
// 检查必填字段
const requiredFields = ['earNumber', 'sex', 'strain', 'varieties', 'cate', 'birthWeight', 'birthday', 'orgId'];
const missingFields = requiredFields.filter(field => !formData[field] && formData[field] !== 0);
if (missingFields.length > 0) {
console.error('缺少必填字段:', missingFields);
message.error(`缺少必填字段: ${missingFields.join(', ')}`);
return;
}
// 处理日期格式 - 转换为时间戳(秒)
const submitData = {
...formData,
birthday: formData.birthday ? formData.birthday.format('YYYY-MM-DD') : null,
weightCalculateTime: formData.weightCalculateTime ? formData.weightCalculateTime.format('YYYY-MM-DD') : null
birthday: formData.birthday ? Math.floor(formData.birthday.valueOf() / 1000) : null,
weightCalculateTime: formData.weightCalculateTime ? Math.floor(formData.weightCalculateTime.valueOf() / 1000) : null
}
// 如果是新增操作移除id字段
@@ -815,6 +968,11 @@ const handleSubmit = async () => {
console.log('服务器响应:', response);
if (response.success) {
// 如果创建成功且有耳标号,尝试绑定耳标
if (!isEdit.value && formData.earNumber) {
await bindEarTag(formData.earNumber, response.data.id)
}
message.success(isEdit.value ? '更新牛只档案成功' : '创建牛只档案成功')
modalVisible.value = false
await fetchAnimals()
@@ -1210,6 +1368,8 @@ onMounted(() => {
fetchFarms()
fetchPens()
fetchBatches()
fetchCattleUsers()
fetchCattleTypes()
})
</script>

View File

@@ -119,18 +119,32 @@
</template>
</a-input>
<a-select
v-model="searchType"
placeholder="围栏类型"
class="type-filter"
@change="handleSearch"
v-model="selectedFenceName"
placeholder="选择围栏名称"
class="fence-name-select"
@change="handleFenceNameSelect"
allowClear
show-search
:filter-option="filterFenceNameOption"
:loading="fenceNamesLoading"
>
<a-select-option value="">全部类型</a-select-option>
<a-select-option value="grazing">放牧区</a-select-option>
<a-select-option value="safety">安全区</a-select-option>
<a-select-option value="restricted">限制区</a-select-option>
<a-select-option value="collector">收集区</a-select-option>
<a-select-option
v-for="fence in fenceList"
:key="fence.id"
:value="fence.name"
>
{{ fence.name }}
</a-select-option>
</a-select>
<a-button
type="default"
@click="clearAllSearch"
class="clear-search-btn"
:disabled="!searchValue && !selectedFenceName && !searchType"
>
清除搜索
</a-button>
</div>
<div class="fence-stats">
<a-statistic
@@ -264,6 +278,8 @@ const submitting = ref(false)
const fenceModalVisible = ref(false)
const searchValue = ref('')
const searchType = ref('')
const selectedFenceName = ref('')
const fenceNamesLoading = ref(false)
const selectedFence = ref(null)
const editingFence = ref(null)
@@ -306,8 +322,13 @@ let allMarkers = []
const filteredFenceList = computed(() => {
let filtered = fenceList.value
// 按名称和描述搜索
if (searchValue.value) {
// 按围栏名称选择过滤
if (selectedFenceName.value) {
filtered = filtered.filter(fence => fence.name === selectedFenceName.value)
}
// 按名称和描述搜索(当没有选择具体围栏名称时)
if (searchValue.value && !selectedFenceName.value) {
const searchLower = searchValue.value.toLowerCase()
filtered = filtered.filter(fence =>
fence.name.toLowerCase().includes(searchLower) ||
@@ -700,7 +721,7 @@ const handleFenceSubmit = async () => {
// 保存围栏
const result = await api.electronicFence.createFence(fenceData)
if (result && result.id) {
if (result && result.data && result.data.id) {
// 保存坐标点
const pointsData = drawingState.currentPoints.map((point, index) => ({
lng: point.lng,
@@ -709,10 +730,16 @@ const handleFenceSubmit = async () => {
description: `围栏点${index + 1}`
}))
await api.electronicFencePoints.createPoints({
fence_id: result.id,
points: pointsData
})
try {
await api.electronicFencePoints.createPoints({
fence_id: result.data.id,
points: pointsData
})
console.log('坐标点创建成功')
} catch (pointError) {
console.error('坐标点创建失败:', pointError)
message.warning('围栏创建成功,但坐标点保存失败: ' + pointError.message)
}
console.log('围栏创建成功:', result)
message.success('围栏创建成功')
@@ -1090,6 +1117,27 @@ const handleSearch = () => {
// 搜索逻辑在计算属性中处理
}
// 处理围栏名称选择
const handleFenceNameSelect = (value) => {
selectedFenceName.value = value
// 清空文本搜索,避免冲突
if (value) {
searchValue.value = ''
}
}
// 过滤围栏名称选项
const filterFenceNameOption = (input, option) => {
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
// 清除所有搜索条件
const clearAllSearch = () => {
searchValue.value = ''
selectedFenceName.value = ''
searchType.value = ''
}
// 初始化
onMounted(() => {
console.log('电子围栏组件已挂载')
@@ -1233,15 +1281,26 @@ onMounted(() => {
.search-section {
margin-bottom: 15px;
display: flex;
flex-direction: column;
gap: 10px;
}
.search-input {
margin-bottom: 10px;
width: 100%;
}
.fence-name-select {
width: 100%;
}
.type-filter {
width: 100%;
margin-bottom: 10px;
}
.clear-search-btn {
width: 100%;
margin-top: 5px;
}
.fence-stats {