农产品种植备案

This commit is contained in:
沈鸿 2025-06-19 13:33:54 +08:00
parent f09b8efa2e
commit 8a28f6f9bb
7 changed files with 814 additions and 13 deletions

View File

@ -13,8 +13,8 @@ VITE_APP_UPLOAD_API = '/uploadApis'
# VITE_APP_UPLOAD_URL = 'http://47.109.205.240:9204'
# 内网测试库接口地址
VITE_APP_BASE_URL = 'http://192.168.18.99:8080'
# VITE_APP_BASE_URL = 'http://192.168.18.99:8080'
# 本地开发接口地址
# VITE_APP_BASE_URL = 'http://192.168.18.74:8080'
# VITE_APP_UPLOAD_URL = 'http://192.168.18.74:8080'
VITE_APP_BASE_URL = 'http://192.168.18.74:8080'
VITE_APP_UPLOAD_URL = 'http://192.168.18.74:8080'

View File

@ -11,19 +11,12 @@
@remove-tag="$emit('remove-tag', $event)"
@scroll="$emit('scroll', $event)"
>
<!-- 1. 原生 prefix 插槽 -->
<slot name="prefix" />
<!-- 2. 如果要实现分组插槽也照搬 el-option-group 的结构 -->
<template v-for="(_, name) in $slots" #[name]="scopedData">
<slot :name="name" v-bind="scopedData"> </slot>
</template>
<slot name="option-group">
<!-- 默认选项渲染 -->
<el-option v-for="item in options" :key="item[valueKey]" :label="item[labelKey]" :value="item[valueKey]" />
</slot>
<!-- 3. 其他自定义插槽不指定 name 的默认为 default slot -->
<slot />
<!-- 4. 下面这几个插槽是 el-select 也常见的 -->
<slot name="empty" />
<slot name="no-match" />
<slot name="footer" />
</el-select>
</template>

View File

@ -0,0 +1,113 @@
<template>
<el-dialog :v-model="visible" :title="dialogTitle" width="600px" destroy-on-close :close-on-click-modal="false">
<el-form ref="formRef" :model="formModel" :rules="rules" :disabled="isView" label-width="100px">
<el-form-item label="地块名称" prop="name">
<el-input v-model="formModel.name" placeholder="请输入地块名称" />
</el-form-item>
<el-form-item label="所属区域" prop="areaCode">
<AreaSelect v-model="formModel.areaCode" :emit-path="false" />
</el-form-item>
<el-form-item label="面积(亩)" prop="area">
<el-input-number v-model="formModel.area" :min="0" placeholder="请输入面积" />
</el-form-item>
<el-form-item label="图像文件" prop="image">
<FileUploader v-model="formModel.image" :limit="1" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formModel.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button v-if="!isView" type="primary" :loading="submitting" @click="submit">保存</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
initialData: {
type: Object,
default: () => ({}),
},
mode: {
type: String,
default: 'create', // 'create' | 'edit' | 'view'
},
type: {
type: String,
default: '', // grass / forest / field
},
});
const emit = defineEmits(['update:visible', 'submit']);
const formRef = ref(null);
const formModel = ref({});
const submitting = ref(false);
const isView = computed(() => props.mode === 'view');
const dialogTitle = computed(() => {
switch (props.mode) {
case 'create':
return '新增地块';
case 'edit':
return '编辑地块';
case 'view':
return '查看地块';
default:
return '地块信息';
}
});
const rules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
areaCode: [{ required: true, message: '请选择区域', trigger: 'change' }],
area: [{ required: true, message: '请输入面积', trigger: 'blur' }],
};
watch(
() => props.visible,
(val) => {
if (val) {
formModel.value = { ...props.initialData };
}
}
);
function close() {
emit('update:visible', false);
}
async function submit() {
if (!formRef.value) return;
try {
await formRef.value.validate();
submitting.value = true;
emit('submit', { ...formModel.value });
} catch (err) {
console.warn('表单校验失败', err);
} finally {
submitting.value = false;
}
}
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<div class="land-page">
<SearchBar v-model:search="searchForm" @search="handleSearch" @reset="handleReset" />
<el-tabs v-model="activeTab" @tab-click="handleTabChange">
<!-- <el-tab-pane label="待提交" name="-1" /> -->
<el-tab-pane label="待审核" name="0" />
<el-tab-pane label="已通过" name="1" />
<el-tab-pane label="已驳回" name="2" />
</el-tabs>
<avue-crud
ref="crudRef"
v-model:page="pagination"
:data="crudData"
:option="crudOptions"
:table-loading="loading"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
>
<template #menu="scope">
<custom-table-operate :actions="crudOptions.actions" :data="scope" />
</template>
</avue-crud>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch } from 'vue';
import SearchBar from './SearchBar.vue';
import { CRUD_OPTIONS } from '@/config';
import FormDialog from './FormDialog.vue';
const loading = ref(false);
const visible = ref(false);
const searchForm = ref({});
const activeTab = ref('1');
const handleTabChange = ({ name }) => {
// searchForm.value.landStatus = Number(activeTab.value);
getData();
};
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const crudData = ref([]);
const crudOptions = reactive({
...CRUD_OPTIONS,
header: false,
height: 'calc(100vh - 360px)',
column: [
{ label: '地块编号', prop: 'id', width: 160 },
{ label: '地块名称', prop: 'landName', width: 170 },
{ label: '面积', prop: 'area', formatter: (row, column, cellValue) => `${Number(cellValue).toFixed(2)}` },
{ label: '所属行政区域', prop: 'fullRegionName', width: 160 },
{ label: '所属网格', prop: 'gridName', width: 90 },
{ label: '具体位置', prop: 'address', width: 160 },
{ label: '种植作物', prop: 'CropName' },
{ label: '作物品种', prop: 'CropVarietyName' },
{ label: '种植开始时间', prop: 'plantingStartTime' },
{ label: '种植结束时间', prop: 'plantingEndTime' },
{ label: '经营主体名称', prop: 'businessSubjectName', width: 130 },
{ label: '账号(手机号)', prop: ' account', width: 130 },
{ label: '信息填报时间', prop: 'fillTime', width: 110 },
{ label: '信息审核时间', prop: 'auditTime', width: 110 },
],
});
const handleSearch = () => {};
const handleReset = () => {
getData();
};
const getData = () => {
//
//
// list total
};
</script>

View File

@ -0,0 +1,69 @@
<template>
<div class="custom-page-search">
<el-form :model="search" inline label-width="100px">
<el-form-item label="关键词搜索" prop="keyword">
<el-input v-model="search.keyword" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="" label-width="0px">
<AreaCascader v-model:region-code="search.regionCode" v-model:grid-id="search.id" />
</el-form-item>
<el-form-item label="种植作物" prop="planCrop">
<url-select
v-model="search.planCrop"
url="/land-resource/crops/page"
:params="{ status: '0' }"
label-key="cropsName"
value-key="id"
placeholder="请选择种植作物"
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">搜索</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { reactive, watch, toRefs } from 'vue';
const props = defineProps({
search: {
type: Object,
required: true,
},
});
const emit = defineEmits(['update:search', 'search', 'reset']);
const search = reactive({ ...props.search });
watch(
search,
(val) => {
emit('update:search', { ...val });
},
{ deep: true }
);
const onSearch = () => {
emit('search');
};
const onReset = () => {
Object.keys(search).forEach((key) => {
search[key] = '';
});
emit('update:search', { ...search });
emit('reset');
};
</script>
<style scoped>
.custom-page-search {
margin-bottom: 16px;
}
</style>

View File

@ -0,0 +1,9 @@
<template>
<div class="custom-page">
<RecordPage />
</div>
</template>
<script setup>
import RecordPage from './components/RecordPage.vue';
</script>

View File

@ -0,0 +1,537 @@
<template>
<div class="custom-page">
<h1>农产品种植品种备案</h1>
<!-- 搜索 -->
<el-form :inline="true" :model="searchForm" class="search-bar">
<el-form-item label="关键词">
<el-input v-model="searchForm.name" placeholder="请输入关键词" clearable />
</el-form-item>
<el-form-item lable="">
<AreaCascader v-model:region-code="searchForm.regionCode" v-model:grid-id="searchForm.gridId" :width="600" />
</el-form-item>
<el-form-item label="种植作物">
<url-select
v-model="searchForm.cropId"
placeholder="选择种植作物"
url="/land-resource/baseInfo/cropPage"
:params="{ current: 1, size: 100 }"
label-key="cropName"
value-key="id"
:clearable="true"
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch"> 搜索 </el-button>
<el-button @click="resetSearch"> 重置 </el-button>
</el-form-item>
</el-form>
<!-- 四个固定 Tabs -->
<el-tabs v-model="activeTab" @tab-click="handleTabChange">
<!-- <el-tab-pane label="待提交" name="-1" /> -->
<el-tab-pane label="待审核" name="0" />
<el-tab-pane label="已通过" name="1" />
<el-tab-pane label="已驳回" name="2" />
</el-tabs>
<avue-crud
ref="crudRef"
v-model:page="pageData"
:data="crudData"
:option="crudOptions"
:table-loading="loading"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
>
<template #menu="scope">
<custom-table-operate :actions="crudOptions.actions" :data="scope" />
</template>
</avue-crud>
<el-dialog :key="dialogTitle" v-model="visible" :title="dialogTitle" width="60%" align-center :draggable="true">
<el-tabs v-model="activeFormTab" class="tabs-wrapper">
<el-tab-pane label="土地基本信息" name="basic">
<p class="form-title">基本信息</p>
<el-form ref="basicFormRef" :model="formData" :disabled="isReadonly" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="地块名称" prop="landName">
<el-input v-model="formData.landName" placeholder="请输入地块名称" />
</el-form-item>
<el-form-item label="土地类型" prop="landType">
<el-tree-select
v-model="formData.landType"
:data="landTypeOptions"
:props="treeProps"
placeholder="选择土地类型"
clearable
check-strictly
:render-after-expand="false"
@change="handleLandTypeChange"
/>
</el-form-item>
<el-form-item label="具体位置" prop="address">
<el-input v-model="formData.address" placeholder="请输入具体位置" />
</el-form-item>
<el-form-item label="土壤类型" prop="soilTypeId">
<url-select
v-model="formData.soilTypeId"
placeholder="选择土壤类型"
url="/land-resource/baseInfo/soilTypePage"
:params="{ current: 1, size: 100 }"
label-key="soilType"
value-key="id"
:clearable="true"
/>
</el-form-item>
<el-form-item label="土地照片" prop="landUrl">
<FileUploader v-model="formData.landUrl" :limit="1" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="面积(亩)" prop="area">
<el-input-number v-model="formData.area" :min="0" :precision="2" :step="0.1" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="" label-width="0" prop="gridId">
<AreaCascader v-model:region-code="formData.gridAreaCode" v-model:grid-id="formData.gridId" label="" :split-rows="true" />
</el-form-item>
<el-form-item label="土地范围" prop="scope">
<el-input v-model="formData.scope" placeholder="请输入土地范围" />
<!-- <Attrs v-model:attrs="formData.scope" type="add" accept="image/*" /> -->
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="土地承包信息" name="property">
<p class="form-title">承包信息</p>
<el-form ref="propertyFormRef" :model="formData" :disabled="isReadonly" label-width="150px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="土地承包经营权人" prop="propertyName">
<el-input v-model="formData.propertyName" placeholder="请输入产权人姓名" />
</el-form-item>
<el-form-item label="联系方式" prop="propertyPhone">
<el-input v-model="formData.propertyPhone" placeholder="请输入产权人联系方式" />
</el-form-item>
<el-form-item label="土地经营权证书编号" prop="landCode">
<el-input v-model="formData.landCode" placeholder="请输入产权编号" />
</el-form-item>
<el-form-item label="土地经营权证书" prop="propertyCertificateUrl">
<FileUploader v-model="formData.propertyCertificateUrl" :limit="1" />
</el-form-item>
</el-col>
<!-- <el-col :span="12"></el-col> -->
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="土地流转信息" name="use">
<p class="form-title">流转信息</p>
<el-form ref="useForm" :model="formData" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="是否土地流转" prop="landTransfer">
<el-radio-group v-model="formData.landTransfer" :disabled="isReadonly">
<el-radio label="1"></el-radio>
<el-radio label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.landTransfer === '1'" label="土地受让方" prop="landUseName">
<el-input v-model="formData.landUseName" placeholder="请输入土地受让方" />
</el-form-item>
<el-form-item v-if="formData.landTransfer === '1'" label="联系电话" prop="landUsePhone">
<el-input v-model="formData.landUsePhone" placeholder="请输入土地受让方联系方式" />
</el-form-item>
<el-form-item v-if="formData.landTransfer === '1'" label="流转合同" prop="landCertificateUrl">
<FileUploader v-model="formData.landCertificateUrl" :limit="1" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
</el-tabs>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<template v-if="!isReadonly">
<el-button type="primary" @click="submitAll">修改</el-button>
</template>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
// ---------------------------------------------------------------------
// avue-crud
// ---------------------------------------------------------------------
import { ref, reactive, watch, onMounted, computed, nextTick } from 'vue';
import { CRUD_OPTIONS } from '@/config';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useUserStore } from '@/store/modules/user';
import request from '@/utils/axios';
const UserStore = useUserStore();
const user = UserStore.getUserInfo();
console.log('admin 属性:', user.admin);
const loading = ref(false);
const visible = ref(false);
const isReadonly = ref(false);
const dialogTitle = ref();
const activeTab = ref('1');
const activeFormTab = ref('basic');
const formData = ref({
gridName: '',
gridAreaCode: '',
scope: '',
scopeImg: '',
note: '',
});
const initialFormData = { ...formData.value };
const resetForm = () => {
formData.value = { ...initialFormData };
};
const pageData = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const searchForm = ref({
keyword: '',
landStatus: 1,
regionCode: '',
gridId: '',
id: '',
landTypeName: '草地',
});
const initialSearchForm = { ...searchForm.value };
const resetSearch = () => {
searchForm.value = { ...initialSearchForm };
};
//
const filterObject = (obj) => {
const newObj = {};
Object.keys(obj).forEach((key) => {
const value = obj[key];
// null undefined
if (value !== '' && value !== null && value !== undefined) {
newObj[key] = value;
}
});
return newObj;
};
const crudData = ref([]);
const crudOptions = reactive({
...CRUD_OPTIONS,
addBtn: false,
header: false,
searchBtn: false,
emptyBtn: false,
refreshBtn: false,
height: 'calc(100vh - 360px)',
column: [
{ label: '地块编号', prop: 'id', width: 160 },
{ label: '地块名称', prop: 'landName', width: 170 },
{ label: '面积', prop: 'area', formatter: (row, column, cellValue) => `${Number(cellValue).toFixed(2)}` },
{ label: '所属行政区域', prop: 'fullRegionName', width: 160 },
{ label: '所属网格', prop: 'gridName', width: 90 },
{ label: '具体位置', prop: 'address', width: 160 },
{ label: '种植作物', prop: 'CropName' },
{ label: '作物品种', prop: 'CropVarietyName' },
{ label: '种植开始时间', prop: 'plantingStartTime' },
{ label: '种植结束时间', prop: 'plantingEndTime' },
{ label: '经营主体名称', prop: 'businessSubjectName', width: 130 },
{ label: '账号(手机号)', prop: ' account', width: 130 },
{ label: '信息填报时间', prop: 'fillTime', width: 110 },
{ label: '信息审核时间', prop: 'auditTime', width: 110 },
],
actions: [
{
name: '查看',
icon: 'view',
event: ({ row }) => handleView(row),
},
{
name: '编辑',
icon: 'edit',
event: ({ row }) => handleEdit(row),
},
{
type: 'danger',
name: '删除',
icon: 'delete',
event: ({ row }) => handleDelete(row),
},
{
name: '审核',
icon: 'approve',
event: ({ row }) => onApprove(row),
},
// TODO:
{
name: '驳回',
icon: 'reject',
event: ({ row }) => onReject(row),
},
{
name: '驳回原因',
icon: 'reject',
event: ({ row }) => onRejectReason(row),
},
],
});
const handleTabChange = ({ name }) => {
searchForm.value.landStatus = Number(activeTab.value);
getData();
};
const handleRefresh = async () => {
searchForm.value = { ...initialSearchForm };
getData();
};
const handleCurrentChange = (val) => {
pageData.value.currentPage = val;
};
const handleSizeChange = (val) => {
pageData.value.pageSize = val;
};
const handleView = async (row) => {
isReadonly.value = true;
formData.value = await getLandDetail(row.id);
dialogTitle.value = '查看网格';
visible.value = true;
};
const handleEdit = (row) => {
isReadonly.value = false;
formData.value = { ...row };
dialogTitle.value = '编辑网格';
visible.value = true;
};
const handleDelete = async (row) => {
console.log('删除', row);
// try {
// await ElMessageBox.confirm('', '', {
// confirmButtonText: '',
// cancelButtonText: '',
// type: 'warning',
// });
// const response = await deleteGrid(row.id);
// ElMessage.success('');
// getData();
// } catch (error) {
// if (error === 'cancel') {
// ElMessage.info('');
// } else {
// ElMessage.error('');
// console.error(':', error);
// }
// }
};
const handleSubmit = async () => {
console.log('提交表单:', formData.value);
try {
if (dialogTitle.value === '新增网格') {
await createLand(formData.value);
ElMessage.success('新增成功');
resetForm();
visible.value = false;
getData();
} else {
await editLand(formData.value);
ElMessage.success('更新成功');
resetForm();
visible.value = false;
getData();
}
} catch (error) {
ElMessage.error(error.message || '新增失败,请重试');
}
};
// ---------------------------------------------------------------------
//
// ---------------------------------------------------------------------
import { createLand, deleteLand, editLand, fetchLandList, getLandById, approveLand } from '@/apis/landResourceManagement/landManagement';
onMounted(() => {
getData();
fetchLandTypeData();
});
const getData = async () => {
loading.value = true;
const filteredParams = filterObject(searchForm.value);
const response = await fetchLandList(filteredParams);
crudData.value = Array.isArray(response.data.records) ? response.data.records : [];
pageData.value.total = crudData.value.length;
loading.value = false;
};
const getLandDetail = async (id) => {
const response = await getLandById(id);
return response.data;
};
const landTypeOptions = ref([]);
const treeProps = ref({
value: 'id',
label: 'landType',
children: 'children',
// disabled: (data) => {
// return data.children && data.children.length > 0;
// },
});
const fetchLandTypeData = async () => {
try {
const response = await request.get('/land-resource/baseInfo/landTree', { params: { status: '1' } });
if (response.code === 200) {
landTypeOptions.value = response.data;
}
} catch (error) {
console.error('获取土地类型数据失败', error);
}
};
const submitAll = async () => {
console.log('提交表单:', formData.value);
await editLand(formData.value);
ElMessage.success('保存成功');
resetForm();
visible.value = false;
};
const onApprove = async (row) => {
// UNSUBMIT(-1,""),
// REVIEW(0,""),
// PASS(1,""),
// REFUSE(2,"");
if (row.landStatus !== 0) {
ElMessage.error('该地块状态不是待审核状态');
return;
}
const requiredBody = {
bizType: 'landResInfo',
bizId: row.id,
status: 1,
opinion: '',
};
await approveLand(requiredBody);
ElMessage.success('审核成功');
getData();
};
const onReject = async (row) => {
if (row.landStatus !== 0) {
ElMessage.error('该地块状态不是待审核状态');
return;
}
const requiredBody = {
bizType: 'landResInfo',
bizId: row.id,
status: 2,
opinion: '',
};
await approveLand(requiredBody);
ElMessage.success('驳回成功');
getData();
};
//
const onRejectReason = async (row) => {
ElMessageBox.alert(row.rejectReason || '无驳回原因', '驳回原因');
};
const handleAdd = () => {
console.log('handleAdd');
resetForm();
isReadonly.value = false;
dialogTitle.value = '新增网格';
visible.value = true;
};
const handleSearch = () => {
getData();
};
const handleCancel = () => {
visible.value = false;
};
</script>
<style scoped lang="scss">
.custom-page {
padding-bottom: 0px;
}
h1 {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.search-bar {
padding-left: 20px;
}
:deep(.el-dialog__body) {
padding: 20px;
// height: calc(100vh - 300px);
overflow-y: auto;
}
.form-title {
font-size: 16px;
font-weight: 500;
margin: 30px 0;
color: #333333;
}
.form-item {
width: 500px;
margin: 0 auto;
}
.dialog-footer {
text-align: center;
}
.custom-search {
display: flex;
align-items: center;
padding-left: 20px;
.el-button {
margin-left: 12px;
}
}
.tabs-wrapper {
width: 100%;
display: flex;
align-items: center;
// background-color: #7daaaa;
.el-tabs__header {
width: 100%;
.el-tabs__nav-scroll {
display: flex;
justify-content: center;
}
.el-tabs__item {
font-size: 16px;
color: #555555;
font-weight: 500;
}
}
:deep(.el-tabs__content) {
padding: 20px;
// background-color: #af8686;
border-radius: 4px;
width: 80%;
height: calc(100vh - 400px);
overflow-y: auto;
.el-tab-pane {
margin: 0 auto;
width: 100%;
// background-color: #555555;
}
}
}
</style>