Files
zhyc-sheep-ui/src/views/produce/other/castrate/index.vue
2026-03-05 21:39:44 +08:00

661 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container"
style="height: calc(100vh - 84px); display: flex; flex-direction: column; overflow: hidden;">
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="事件日期" style="width: 308px">
<el-date-picker v-model="daterangeEventDate" value-format="YYYY-MM-DD" type="daterange" range-separator="-"
start-placeholder="开始日期" end-placeholder="结束日期" />
</el-form-item>
<el-form-item label="管理耳号" prop="manageTagsList">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<el-select v-model="queryParams.manageTagsList" multiple filterable remote reserve-keyword
placeholder="输入耳号搜索" :remote-method="searchEarNumber" :loading="earLoading" allow-create
default-first-option collapse-tags :max-collapse-tags="0" style="width:260px" @change="handleEarChange">
<el-option v-for="item in earOptions" :key="item" :label="item" :value="item" />
</el-select>
<el-input v-model="pasteText" placeholder="或粘贴多个耳号(空格/换行/逗号分隔)" style="width:260px" @paste="handlePaste"
@keyup.enter="handlePasteSubmit" clearable>
<template #append>
<el-button @click="handlePasteSubmit" :icon="Plus">添加</el-button>
</template>
</el-input>
<el-tag v-if="queryParams.manageTagsList.length" type="info" effect="plain" size="large">
已选: {{ queryParams.manageTagsList.length }}
</el-tag>
<el-button type="danger" plain :icon="Delete" @click="clearEarNumbers"
v-if="queryParams.manageTagsList.length">
清空全部
</el-button>
</div>
<div v-if="queryParams.manageTagsList.length" class="selected-ear-numbers">
<el-tag v-for="tag in displayedEarTags" :key="tag" closable @close="removeEarNumber(tag)" style="margin:4px"
type="success">
{{ tag }}
</el-tag>
<el-button v-if="queryParams.manageTagsList.length > defaultShowCount" type="primary" link
@click="toggleExpand">
{{ isExpanded ? '收起' : `展开剩余 ${queryParams.manageTagsList.length - defaultShowCount} ` }}
<el-icon class="el-icon--right">
<component :is="isExpanded ? ArrowUp : ArrowDown" />
</el-icon>
</el-button>
</div>
</el-form-item>
<!-- <el-form-item label="羊舍" prop="sheepfold">
<el-select v-model="queryParams.sheepfold" placeholder="请选择羊舍" style="min-width:150px" clearable>
<el-option v-for="item in sheepfoldOptions" :key="item.id" :label="item.sheepfoldName" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="品种" prop="varietyId">
<el-select v-model="queryParams.varietyId" placeholder="请选择品种" style="min-width:150px" clearable>
<el-option v-for="item in varietyOptions" :key="item.id" :label="item.variety" :value="item.id" />
</el-select>
</el-form-item> -->
<el-form-item label="技术员" prop="technician">
<el-select v-model="queryParams.technician" placeholder="请选择技术员" clearable filterable style="max-width: 160px">
<el-option v-for="item in technicalOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="是否在群" prop="isDelete">
<el-select v-model="queryParams.isDelete" placeholder="全部" clearable style="min-width:120px">
<el-option label="全部" value="" />
<el-option label="在群" :value="0" />
<el-option label="离群" :value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<!-- <el-button icon="Refresh" @click="resetQuery">重置</el-button> -->
</el-form-item>
</el-form>
<!-- 按钮区域 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd"
v-hasPermi="['produce:castrate:add']">新增</el-button>
</el-col>
<!-- 按需添加 是否需要修改功能 -->
<!-- <el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate"
v-hasPermi="['produce:castrate:edit']">修改</el-button>
</el-col> -->
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete"
v-hasPermi="['produce:castrate:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport"
v-hasPermi="['produce:castrate:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
</el-row>
<!-- 列表表格 -->
<div style="flex: 1; overflow: hidden;">
<el-table v-loading="loading" :data="castrateList" @selection-change="handleSelectionChange" height="100%"
style="width: 100%">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="耳号" align="center" prop="manageTags" />
<el-table-column label="品种" align="center" prop="varietyName" />
<el-table-column label="事件类型" align="center" prop="eventType" width="120" />
<el-table-column label="去势日期" align="center" prop="eventDate" width="130">
<template #default="scope">
<span>{{ parseTime(scope.row.eventDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="羊舍" align="center" prop="sheepfoldName" />
<el-table-column label="技术员" align="center" prop="technician" />
<el-table-column label="创建人" align="center" prop="createBy" />
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="comment" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<!-- 按需添加 是否需要修改功能 -->
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
v-hasPermi="['produce:castrate:edit']">修改</el-button> -->
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
v-hasPermi="['produce:castrate:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div style="flex-shrink: 0; padding: 10px 0;">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" :page-sizes="[20, 50, 100, 200, 500, 1000, 2000]" />
</div>
<!-- 新增/修改弹窗 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="castrateRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="管理耳号" prop="manageTags">
<el-input v-model="form.manageTags" placeholder="请输入管理耳号" @blur="onManageTagsBlur" clearable />
</el-form-item>
<!-- 新增弹窗已选耳号展示区 -->
<div v-if="batchTags.length" class="selected-ear-numbers">
<el-tag v-for="(tag, idx) in batchTags" :key="tag" closable @close="removeBatchTag(idx)" style="margin:4px"
type="success">
{{ tag }}
</el-tag>
<el-button type="primary" link @click="clearBatchTags">
一键清空 ({{ batchTags.length }})
</el-button>
</div>
<el-form-item label="事件日期" prop="eventDate">
<el-date-picker v-model="form.eventDate" value-format="YYYY-MM-DD" type="date" placeholder="请选择事件日期" />
</el-form-item>
<el-form-item label="羊舍" prop="sheepfold">
<el-select v-model="form.sheepfold" placeholder="请选择或输入羊舍" clearable filterable
:filter-method="filterSheepfold" @change="loadSheepBySheepfold">
<el-option v-for="fold in filteredSheepfoldOptions" :key="fold.id" :label="fold.sheepfoldName"
:value="fold.id" />
</el-select>
</el-form-item>
<el-form-item label="技术员" prop="technician">
<el-select v-model="form.technician" placeholder="请选择技术员" clearable filterable style="width: 100%">
<el-option v-for="item in technicalOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="备注" prop="comment">
<el-input v-model="form.comment" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="castrate">
import { listCastrate, getCastrate, delCastrate, addCastrate, searchEarNumbers } from '@/api/produce/other/castrate'
import { checkSheepByManageTags, getVarietyOptions, getSheepBySheepfoldId } from '@/api/produce/other/fixHoof'
import { listSheepfold_management as listSheepfold } from '@/api/fileManagement/sheepfold_management'
import { Plus, Delete, ArrowUp, ArrowDown } from '@element-plus/icons-vue'
import { getUserByPost } from '@/api/common/user'
import { nextTick } from 'vue'
import dayjs from 'dayjs'
const { proxy } = getCurrentInstance()
const castrateList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const daterangeCreateTime = ref([])
const filteredSheepfoldOptions = ref([])
const varietyOptions = ref([])
const daterangeEventDate = ref([])
const batchTags = ref([]) // 本次要新增的多条耳号
const batchSheep = ref([]) // 对应的羊只对象
// 技术员下拉选项
const technicalOptions = ref([])
const pasteText = ref('')
const isExpanded = ref(false)
const defaultShowCount = 2
const displayedEarTags = computed(() => {
const list = queryParams.value.manageTagsList
if (isExpanded.value || list.length <= defaultShowCount) return list
return list.slice(0, defaultShowCount)
})
const data = reactive({
form: {
manageTags: [],
sheepfold: null,
sheepfoldDisabled: false,
technician: null,
tagDetails: {},
eventDate: null,
comment: null,
},
queryParams: {
pageNum: 1,
pageSize: 20,
manageTags: null,
// sheepId: null,
sheepfold: null,
varietyId: null,
technician: null,
createTime: null,
beginEventDate: null,
endEventDate: null,
manageTagsList: [],
isDelete: null
},
rules: {
manageTags: [
{ required: true, message: '请输入管理耳号', trigger: 'blur' }
],
technician: [
{ required: true, message: '请输入技术员', trigger: 'blur' }
],
eventDate: [
{ required: true, message: '请选择事件日期', trigger: 'change' }
]
}
})
const { queryParams, form, rules } = toRefs(data)
const earOptions = ref([])
const earLoading = ref(false)
// 羊舍变化处理:自动加载该羊舍下所有公羊(清空已有)
function loadSheepBySheepfold() {
const sheepfoldId = form.value.sheepfold
if (!sheepfoldId) {
// 清空羊舍时,不清空耳号
return
}
// 清空已有耳号
batchTags.value = []
batchSheep.value = []
form.value.manageTags = ''
getSheepBySheepfoldId(sheepfoldId)
.then(res => {
let sheepList = res.data || []
sheepList = sheepList.filter(sheep => sheep.gender === 2)
if (sheepList.length === 0) {
proxy.$modal.msgInfo('该羊舍下暂无公羊')
return
}
// 填充该羊舍所有公羊
for (const sheep of sheepList) {
batchTags.value.push(sheep.manageTags)
batchSheep.value.push(sheep)
}
form.value.manageTags = batchTags.value.join(' ')
proxy.$modal.msgSuccess(`已加载 ${batchTags.value.length} 只公羊`)
})
.catch(error => {
console.error('加载羊舍耳号失败', error)
proxy.$modal.msgError('加载耳号失败,请重试')
})
}
// 输入耳号后校验并追加(不清空已有)
async function onManageTagsBlur() {
const raw = form.value.manageTags?.trim()
if (!raw) return
// 按分隔符拆分
const separators = /[\p{White_Space},]+/u
const newTags = [...new Set(raw.split(separators).filter(v => v))]
// 过滤掉已存在的
const existTags = new Set(batchTags.value)
const tagsToAdd = newTags.filter(tag => !existTags.has(tag))
if (tagsToAdd.length === 0) {
// 全部已存在,回写现有
form.value.manageTags = batchTags.value.join(' ')
return
}
// 校验并追加新耳号
for (const tag of tagsToAdd) {
const sheep = await checkSheepByManageTags(tag).then(r => r.data).catch(() => null)
if (!sheep) {
proxy.$modal.msgWarning(`耳号 ${tag} 不存在,已跳过`)
continue
}
if (Number(sheep.gender)) {
proxy.$modal.msgWarning(`耳号 ${tag} 不是公羊,已跳过`)
continue
}
batchTags.value.push(tag)
batchSheep.value.push(sheep)
}
// 回写所有耳号
form.value.manageTags = batchTags.value.join(' ')
}
/** 查询列表 */
function getList() {
loading.value = true;
const q = { ...queryParams.value };
if (q.manageTags === '') q.manageTags = null;
q.params = {};
if (daterangeCreateTime.value?.length) {
q.params.beginCreateTime = daterangeCreateTime.value[0];
q.params.endCreateTime = daterangeCreateTime.value[1];
}
if (daterangeEventDate.value?.length) {
q.params.beginEventDate = daterangeEventDate.value[0];
q.params.endEventDate = daterangeEventDate.value[1];
}
if (q.technician) {
q.params.technician = q.technician;
}
listCastrate(q).then(res => {
castrateList.value = res.rows;
console.log("后端返回的列表数据:", res.rows);
total.value = res.total;
loading.value = false;
});
getVarietyOptions({ pageNum: 1, pageSize: 9999 }).then(res => {
varietyOptions.value = res.rows || []
})
}
//取消
function cancel() {
open.value = false
reset()
}
//重置
function reset() {
form.value = {
id: null,
manageTags: null,
sheepfold: null,
technician: null,
eventDate: null,
comment: null
}
batchTags.value = []
batchSheep.value = []
filteredSheepfoldOptions.value = sheepfoldOptions.value
proxy.resetForm('castrateRef')
}
//搜索
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
//重置
function resetQuery() {
daterangeCreateTime.value = []
daterangeEventDate.value = []
queryParams.value.manageTagsList = []
queryParams.value.technician = null
queryParams.value.isDelete = null
proxy.resetForm('queryRef')
handleQuery()
}
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.id)
single.value = selection.length !== 1
multiple.value = !selection.length
}
//新增
function handleAdd() {
reset()
open.value = true
title.value = '添加去势'
form.value.eventDate = dayjs().format('YYYY-MM-DD')
}
//修改
function handleUpdate(row) {
reset();
const _id = row.id || ids.value;
getCastrate(_id).then(res => {
const data = res.data;
const manageTags = Array.isArray(data.manageTags)
? data.manageTags
: [data.manageTags || ''];
form.value = {
...data,
manageTags: manageTags.filter(tag => tag),
};
if (form.value.sheepfold) {
loadSheepBySheepfold();
}
validateAllTags();
open.value = true;
title.value = '修改去势';
});
}
// 校验所有回显的耳号是否存在
async function validateAllTags() {
const tags = form.value.manageTags || [];
if (tags.length === 0) return;
for (const tag of tags) {
try {
const { data } = await checkSheepByManageTags(tag.trim());
if (!data) {
proxy.$modal.msgWarning(`耳号 ${tag} 不存在,已移除`);
form.value.manageTags = form.value.manageTags.filter(t => t !== tag);
}
} catch (error) {
console.error(`校验耳号 ${tag} 失败`, error);
form.value.manageTags = form.value.manageTags.filter(t => t !== tag);
}
}
}
//提交
function submitForm() {
if (batchTags.value.length === 0) {
proxy.$modal.msgError('请输入至少一个有效耳号')
return
}
if (!form.value.technician) {
proxy.$modal.msgError('请输入技术员')
return
}
if (!form.value.eventDate) {
proxy.$modal.msgError('请选择事件日期')
return
}
// 批量新增
Promise.all(
batchTags.value.map((tag, idx) =>
addCastrate({
sheepId: batchSheep.value[idx].id,
manageTags: tag,
sheepfold: batchSheep.value[idx].sheepfoldId,
varietyId: batchSheep.value[idx].varietyId,
technician: form.value.technician,
eventDate: form.value.eventDate,
comment: form.value.comment || ''
})
)
).then(() => {
proxy.$modal.msgSuccess(`成功新增 ${batchTags.value.length} 条去势记录`)
open.value = false
getList()
}).catch(() => { })
}
/* 删除单个批量耳号 */
function removeBatchTag(idx) {
batchTags.value.splice(idx, 1)
batchSheep.value.splice(idx, 1)
form.value.manageTags = batchTags.value.join(' ')
// 不再自动更新羊舍
}
/* 一键清空批量耳号 */
function clearBatchTags() {
batchTags.value = []
batchSheep.value = []
form.value.manageTags = ''
}
//删除
function handleDelete(row) {
const _ids = row.id || ids.value
proxy.$modal.confirm(`是否确认删除这条记录数据`).then(() => {
return delCastrate(_ids)
}).then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
})
}
//导出
function handleExport() {
queryParams.value.ids = ids.value;
try {
proxy.download('/produce/other/castrate/export', { ...queryParams.value }, `去势记录${Date.now()}.xlsx`);
} finally {
queryParams.value.ids = null;
}
}
// 获取技术员列表岗位编码techs
const fetchTechnicalList = () => {
getUserByPost({ postCode: "techs" })
.then(res => {
if (res.code === 200 && Array.isArray(res.data)) {
technicalOptions.value = res.data.map(item => ({
value: item.nickName,
label: item.nickName
}))
} else {
technicalOptions.value = []
}
})
.catch(() => {
technicalOptions.value = []
})
}
//加载羊舍数据
const sheepfoldOptions = ref([])
function getSheepfoldOptions() {
listSheepfold({ pageNum: 1, pageSize: 9999 }).then(res => {
sheepfoldOptions.value = res.rows
filteredSheepfoldOptions.value = res.rows
})
}
// 羊舍模糊过滤
function filterSheepfold(query) {
if (!query) {
filteredSheepfoldOptions.value = sheepfoldOptions.value
return
}
filteredSheepfoldOptions.value = sheepfoldOptions.value.filter(fold =>
fold.sheepfoldName.includes(query)
)
}
const searchEarNumber = async (query) => {
if (!query || query.trim() === '') {
earOptions.value = []
return
}
earLoading.value = true
try {
const res = await searchEarNumbers(query.trim())
if (res.code === 200 && Array.isArray(res.data)) {
earOptions.value = res.data
} else {
earOptions.value = []
}
} catch (error) {
console.error('搜索耳号失败:', error)
earOptions.value = []
proxy.$modal.msgError('搜索耳号失败')
} finally {
earLoading.value = false
}
}
function clearEarNumbers() {
queryParams.value.manageTagsList = []
}
function removeEarNumber(tag) {
const idx = queryParams.value.manageTagsList.indexOf(tag)
if (idx > -1) queryParams.value.manageTagsList.splice(idx, 1)
}
function handlePaste() {
nextTick(() => handlePasteSubmit())
}
function handlePasteSubmit() {
if (!pasteText.value.trim()) return
const separators = /[\s,\n\r\t]+/
const raw = pasteText.value.trim().split(separators).filter(v => v)
const exist = new Set(queryParams.value.manageTagsList)
const adds = []
const dups = []
raw.forEach(v => {
if (!exist.has(v)) { adds.push(v); exist.add(v) }
else dups.push(v)
})
if (adds.length) {
queryParams.value.manageTagsList = [...queryParams.value.manageTagsList, ...adds]
proxy.$modal.msgSuccess(`成功添加 ${adds.length} 个耳号${dups.length ? `,已忽略 ${dups.length} 个重复` : ''}`)
} else if (dups.length) {
proxy.$modal.msgWarning('所有耳号均已存在')
}
pasteText.value = ''
}
function toggleExpand() {
isExpanded.value = !isExpanded.value
}
function handleEarChange(val) {
queryParams.value.manageTagsList = [...new Set(val)]
}
onMounted(() => {
getSheepfoldOptions()
getVarietyOptions()
fetchTechnicalList()
getList()
})
</script>
<style scoped>
.tag-count {
color: #606266;
background-color: #f5f7fa;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
}
.selected-ear-numbers {
max-height: 150px;
overflow-y: auto;
padding: 8px;
background: #f5f7fa;
border-radius: 4px;
border: 1px dashed #dcdfe6;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.selected-ear-numbers::-webkit-scrollbar {
width: 6px;
}
.selected-ear-numbers::-webkit-scrollbar-thumb {
background-color: #dcdfe6;
border-radius: 3px;
}
</style>