日常提交

This commit is contained in:
沈鸿 2025-06-27 09:41:05 +08:00
parent ef57aabc55
commit 0fb4c5772c
15 changed files with 1089 additions and 759 deletions

View File

@ -12,6 +12,7 @@ declare module 'vue' {
'CenterMap copy': typeof import('./src/components/centerMap copy.vue')['default'] 'CenterMap copy': typeof import('./src/components/centerMap copy.vue')['default']
CodeDialog: typeof import('./src/components/code-dialog/index.vue')['default'] CodeDialog: typeof import('./src/components/code-dialog/index.vue')['default']
Components: typeof import('./src/components/index.js')['default'] Components: typeof import('./src/components/index.js')['default']
copy: typeof import('./src/components/centerMap copy.vue')['default']
CurrentTime: typeof import('./src/components/currentTime.vue')['default'] CurrentTime: typeof import('./src/components/currentTime.vue')['default']
CustomBack: typeof import('./src/components/customBack.vue')['default'] CustomBack: typeof import('./src/components/customBack.vue')['default']
CustomCarouselPicture: typeof import('./src/components/custom-carousel-picture/index.vue')['default'] CustomCarouselPicture: typeof import('./src/components/custom-carousel-picture/index.vue')['default']

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@ -14,14 +14,9 @@ import { registerGlobalComponents } from './plugins/globalComponents';
import { registerElIcons } from './plugins/icon'; import { registerElIcons } from './plugins/icon';
import { registerMicroApps } from './plugins/micro'; import { registerMicroApps } from './plugins/micro';
import { registerSplitpanes } from './plugins/splitpanes'; import { registerSplitpanes } from './plugins/splitpanes';
import VueTianditu from 'vue-tianditu';
import { map_config } from './config/map';
const app = createApp(App); const app = createApp(App);
app.use(pinia).use(router).use(ElementPlus).use(Avue).use(VueTianditu, { app.use(pinia).use(router).use(ElementPlus).use(Avue);
v: map_config.tianditu.version,
tk: map_config.tianditu.token,
});
registerGlobalComponents(app); registerGlobalComponents(app);
registerElIcons(app); registerElIcons(app);
registerSplitpanes(app); registerSplitpanes(app);

View File

@ -41,16 +41,23 @@ export const constantRoutes = [
...inputSuppliesRoutes, ...inputSuppliesRoutes,
{ {
path: '/sub-government-affairs-service/output-products', path: '/sub-government-affairs-service/output-products',
name: 'outputProducts', name: 'OutputProducts',
component: Layout, component: Layout,
redirect: '/sub-government-affairs-service/output-products/index', redirect: '/sub-government-affairs-service/output-products/output-info',
meta: { title: '产出品管理', icon: 'Box' }, meta: { title: '产出品管理', icon: 'Box' },
children: [ children: [
// 产出品概览
{ {
path: '/sub-government-affairs-service/output-products/index', path: '/sub-government-affairs-service/output-products/output-statistics',
component: () => import('@/views/outputProductsManage/index.vue'), component: () => import('@/views/output-products/output-statistics/index.vue'),
name: 'outputProductsIndex', name: 'OutputStatistics',
meta: { title: '产出品管理', icon: 'List' }, meta: { title: '产出品概览', icon: 'Box' },
},
{
path: '/sub-government-affairs-service/output-products/output-info',
component: () => import('@/views/output-products/output-info/index.vue'),
name: 'OutputInfo',
meta: { title: '产出品信息', icon: 'List' },
}, },
], ],
}, },

View File

@ -0,0 +1,17 @@
// MapLoader.js
export function loadTiandituApi(config) {
return new Promise((resolve) => {
if (window.T) {
// 已经加载过,直接返回 T
resolve(window.T);
return;
}
const script = document.createElement('script');
script.id = 'tianditu-script';
script.src = `http://api.tianditu.gov.cn/api?v=${config.version}&tk=${config.token}`;
script.onload = () => {
resolve(window.T);
};
document.head.appendChild(script);
});
}

View File

@ -1,5 +1,10 @@
<template> <template>
<div class="custom-page"> <div class="custom-page">
<div class="custom-search">
<AreaCascader v-model:region-code="searchForm.regionCode" v-model:grid-id="searchForm.id" :width="600" />
<el-button type="primary" @click="handleSearch"> 搜索 </el-button>
<el-button @click="resetSearch"> 重置 </el-button>
</div>
<avue-crud <avue-crud
ref="crudRef" ref="crudRef"
v-model:page="pageData" v-model:page="pageData"
@ -10,13 +15,6 @@
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
@size-change="handleSizeChange" @size-change="handleSizeChange"
> >
<template #search>
<div class="custom-search">
<AreaCascader v-model:region-code="searchForm.regionCode" v-model:grid-id="searchForm.id" :width="600" />
<el-button type="primary" @click="handleSearch"> 搜索 </el-button>
<el-button @click="resetSearch"> 重置 </el-button>
</div>
</template>
<!-- <template #menu-left> <!-- <template #menu-left>
<el-button type="primary" icon="Plus" @click="handleAdd">新增网格</el-button> <el-button type="primary" icon="Plus" @click="handleAdd">新增网格</el-button>
</template> --> </template> -->
@ -152,10 +150,10 @@ const filterObject = (obj) => {
const crudData = ref([]); const crudData = ref([]);
const crudOptions = reactive({ const crudOptions = reactive({
...CRUD_OPTIONS, ...CRUD_OPTIONS,
addBtn: false, menu: false,
searchBtn: false, header: false,
emptyBtn: false,
height: 'calc(100vh - 340px)', height: 'calc(100vh - 340px)',
selection: true,
column: [ column: [
{ label: '行政区划编码', prop: 'AreaCode', width: 150 }, { label: '行政区划编码', prop: 'AreaCode', width: 150 },
{ label: '行政区划', prop: 'gridAreaName', width: 150 }, { label: '行政区划', prop: 'gridAreaName', width: 150 },

View File

@ -0,0 +1,16 @@
<template>
<div class="custom-page" :style="`background-image: url(${getAssetsFile('images/output/output.png')})`"></div>
</template>
<script setup>
import { ref, watch, onMounted, computed } from 'vue';
import { getAssetsFile } from '@/utils';
</script>
<style scoped lang="scss">
.custom-page {
height: calc(100vh - 150px);
background-size: cover;
background-repeat: no-repeat;
}
</style>

View File

@ -1,88 +1,207 @@
<template> <template>
<div class="custom-page"> <div class="custom-page">
<h1>林地</h1> <h1>林地</h1>
<!-- 搜索 --> <LandSearch :search="searchForm" @on-search="handleSearch" @on-reset="handleReset" />
<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.id" :width="600" />
</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-tabs v-model="activeTab" @tab-click="handleTabChange">
<!-- <el-tab-pane label="待提交" name="0" /> --> <!-- <el-tab-pane label="待提交" name="0" /> -->
<el-tab-pane label="待审核" name="1" /> <el-tab-pane label="待审核" name="1" />
<el-tab-pane label="已通过" name="2" /> <el-tab-pane label="已通过" name="2" />
<el-tab-pane label="已驳回" name="0" /> <el-tab-pane label="已驳回" name="0" />
</el-tabs> </el-tabs>
<avue-crud <TableComponent
ref="crudRef" :loading="loading"
v-model:page="pageData" :columns="columns"
:data="crudData" :table-data="tableData"
:option="crudOptions" :current-page="pageData.currentPage"
:table-loading="loading" :page-size="pageData.pageSize"
@current-change="handleCurrentChange" :total="pageData.total"
@size-change="handleSizeChange" :show-pagination="true"
> :show-border="true"
<!-- <template #menu-left> :show-sort="true"
<el-button type="primary" icon="Plus" @click="handleAdd">新增网格</el-button> style="height: calc(100vh - 320px)"
</template> --> @page-change="handlePageChange"
<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-dialog :key="dialogTitle" v-model="visible" :title="dialogTitle" width="60%" align-center :draggable="true">
<el-form ref="form" :model="formData" :rules="rules" :disabled="isReadonly" label-width="100px" class="form-item"> <el-tabs v-model="activeFormTab" class="tabs-wrapper">
<p class="form-title">填写网格信息</p> <el-tab-pane label="土地基本信息" name="basic">
<el-form-item label="网格名称" prop="gridName"> <p class="form-title">基本信息</p>
<el-input v-model="formData.gridName" placeholder="请输入网格名称" /> <el-form ref="basicFormRef" :model="formData" :disabled="isReadonly" label-width="120px">
</el-form-item> <el-row :gutter="20">
<el-form-item label="所属行政区域" prop="gridAreaCode"> <el-col :span="12">
<AreaSelect v-model="formData.gridAreaCode" :label="null" :emit-path="false" /> <el-form-item label="地块名称" prop="landName">
</el-form-item> <el-input v-model="formData.landName" placeholder="请输入地块名称" />
<el-form-item label="网格化地图" prop="scopeImg"> </el-form-item>
<FileUploader v-model="formData.scopeImg" :limit="1" /> <el-form-item label="土地类型" prop="landType">
</el-form-item> <el-tree-select
<el-form-item label="备注" prop="note"> v-model="formData.landType"
<el-input v-model="formData.note" placeholder="请输入备注" /> :data="landTypeOptions"
</el-form-item> :props="treeProps"
</el-form> 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> <template #footer>
<div class="dialog-footer"> <span class="dialog-footer">
<el-button @click="handleCancel">取消</el-button> <el-button @click="visible = false">取消</el-button>
<el-button v-if="!isReadonly" type="primary" @click="handleSubmit()"> 保存 </el-button> <template v-if="!isReadonly">
</div> <el-button type="primary" @click="submitAll">修改</el-button>
</template>
</span>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
// --------------------------------------------------------------------- import { ref, onMounted } from 'vue';
// avue-crud import LandSearch from './components/LandSearch.vue';
// --------------------------------------------------------------------- import TableComponent from '@/components/tableComponent.vue';
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 { mockData } from './landData';
const UserStore = useUserStore(); const searchForm = ref({});
const user = UserStore.getUserInfo(); const handleSearch = (searchData) => {
console.log('admin 属性:', user.admin); // 使
console.log(searchData);
};
const handleReset = () => {
//
};
const activeTab = ref('1');
const handleTabChange = ({ name }) => {
// activeTab.value = name;
console.log('切换tab', activeTab.value);
searchForm.value.landStatus = Number(activeTab.value);
getData();
};
const loading = ref(false); const loading = ref(false);
const tableData = ref([]);
const pageData = ref({
currentPage: 1,
pageSize: 10,
total: 0,
});
const columns = ref([
{ label: '地块编号', prop: 'id', width: 160 },
{ label: '地块名称', prop: 'landName', width: 170 },
{ label: '面积', prop: 'area', formatter: (row, column, cellValue) => `${Number(cellValue).toFixed(2)}` },
{ label: '土地分类', prop: 'landTypeName' },
{ label: '土壤类型', prop: 'soilTypeName' },
{ label: '所属行政区域', prop: 'fullRegionName', width: 160 },
{ label: '所属网格', prop: 'gridName', width: 90 },
{ label: '具体位置', prop: 'address', width: 160 },
{ label: '土地承包经营人', prop: 'propertyName' },
{ label: '联系电话', prop: 'propertyPhone' },
{ label: '土地经营权证书编号', prop: 'landCode', width: 160 },
{ label: '是否土地流转', prop: 'isTransfer' },
{ label: '土地受让方', prop: 'transferName' },
{ label: '受让方联系电话', prop: 'transferPhone', width: 100 },
{ label: '流转合同', prop: 'transferContract' },
{ label: '信息填报人', prop: 'fillName' },
{ label: '信息填报单位', prop: 'fillGroup' },
{ label: '信息填报时间', prop: 'fillTime' },
// { label: '', prop: 'updateName' },
// { label: '', prop: 'updateGroup' },
{ label: '信息更新时间', prop: 'updateTime' },
]);
const handlePageChange = ({ page, pageSize }) => {
pageData.value.currentPage = page;
pageData.value.pageSize = pageSize;
getData();
};
const visible = ref(false); const visible = ref(false);
const isReadonly = ref(false); const isReadonly = ref(false);
const dialogTitle = ref(); const dialogTitle = ref();
const activeTab = ref('1'); const activeFormTab = ref('basic');
const formData = ref({ const formData = ref({
gridName: '', gridName: '',
gridAreaCode: '', gridAreaCode: '',
@ -95,165 +214,9 @@ const resetForm = () => {
formData.value = { ...initialFormData }; formData.value = { ...initialFormData };
}; };
const pageData = ref({ import { mockData } from './landData';
currentPage: 1, import { createLand, deleteLand, editLand, fetchLandList, getLandById, approveLand } from '@/apis/landResourceManagement/landManagement';
pageSize: 10, import request from '@/utils/axios';
total: 0,
});
const searchForm = ref({
gridName: '',
keyword: '',
regionCode: '',
id: '',
status: -1,
});
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: 'landTypeName' },
{ label: '土壤类型', prop: 'soilTypeName' },
{ label: '所属行政区域', prop: 'fullRegionName', width: 160 },
{ label: '所属网格', prop: 'gridName', width: 90 },
{ label: '具体位置', prop: 'address', width: 160 },
{ label: '土地承包经营人', prop: 'propertyName' },
{ label: '联系电话', prop: 'propertyPhone' },
{ label: '土地经营权证书编号', prop: 'landCode', width: 160 },
{ label: '是否土地流转', prop: 'isTransfer' },
{ label: '土地受让方', prop: 'transferName' },
{ label: '受让方联系电话', prop: 'transferPhone', width: 100 },
{ label: '流转合同', prop: 'transferContract' },
{ label: '信息填报人', prop: 'fillName' },
{ label: '信息填报单位', prop: 'fillGroup' },
{ label: '信息填报时间', prop: 'fillTime' },
// { label: '', prop: 'updateName' },
// { label: '', prop: 'updateGroup' },
{ label: '信息更新时间', prop: 'updateTime' },
],
actions: [
{
name: '查看',
icon: 'view',
event: ({ row }) => handleView(row),
},
{
name: '编辑',
icon: 'edit',
event: ({ row }) => handleEdit(row),
},
{
type: 'danger',
name: '删除',
icon: 'delete',
event: ({ row }) => handleDelete(row),
},
],
});
const handleTabChange = ({ name }) => {
// activeTab.value = name;
console.log('切换tab', 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 = (row) => {
isReadonly.value = true;
formData.value = { ...row };
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 createGrid(formData.value);
ElMessage.success('新增成功');
resetForm();
visible.value = false;
getData();
} else {
await updateGrid(formData.value);
ElMessage.success('更新成功');
resetForm();
visible.value = false;
getData();
}
} catch (error) {
ElMessage.error(error.message || '新增失败,请重试');
}
};
// ---------------------------------------------------------------------
//
// ---------------------------------------------------------------------
import { createGrid, updateGrid, deleteGrid, fetchGridList, getGridDetail, exportGrid } from '@/apis/landResourceManagement/gridManagement';
onMounted(() => {
getData();
});
const getData = async () => { const getData = async () => {
// const filteredParams = filterObject(searchForm.value); // const filteredParams = filterObject(searchForm.value);
// const response = await fetchGridList(filteredParams); // const response = await fetchGridList(filteredParams);
@ -261,69 +224,50 @@ const getData = async () => {
loading.value = true; loading.value = true;
// //
await new Promise((resolve) => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
crudData.value = mockData tableData.value = mockData
.filter((item) => item.status === activeTab.value) .filter((item) => item.status === activeTab.value)
.map((item) => ({ .map((item) => ({
...item, ...item,
landTypeName: '林地', landTypeName: '林地',
})); }));
pageData.value.total = crudData.value.length; pageData.value.total = tableData.value.length;
loading.value = false; loading.value = false;
}; };
const getLandDetail = async (id) => {
const handleAdd = () => { const response = await getLandById(id);
console.log('handleAdd'); return response.data;
resetForm();
isReadonly.value = false;
dialogTitle.value = '新增网格';
visible.value = true;
}; };
const landTypeOptions = ref([]);
const handleSearch = () => { 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);
}
};
onMounted(() => {
getData(); getData();
}; fetchLandTypeData();
const handleCancel = () => { });
visible.value = false;
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.custom-page { .custom-page {
padding-bottom: 0px; display: flex;
flex-direction: column;
} }
h1 { h1 {
font-size: 20px; font-size: 20px;
font-weight: bold; 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;
}
} }
</style> </style>

View File

@ -0,0 +1,329 @@
<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.id" :width="600" />
</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="0" /> -->
<el-tab-pane label="待审核" name="1" />
<el-tab-pane label="已通过" name="2" />
<el-tab-pane label="已驳回" name="0" />
</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-left>
<el-button type="primary" icon="Plus" @click="handleAdd">新增网格</el-button>
</template> -->
<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-form ref="form" :model="formData" :rules="rules" :disabled="isReadonly" label-width="100px" class="form-item">
<p class="form-title">填写网格信息</p>
<el-form-item label="网格名称" prop="gridName">
<el-input v-model="formData.gridName" placeholder="请输入网格名称" />
</el-form-item>
<el-form-item label="所属行政区域" prop="gridAreaCode">
<AreaSelect v-model="formData.gridAreaCode" :label="null" :emit-path="false" />
</el-form-item>
<el-form-item label="网格化地图" prop="scopeImg">
<FileUploader v-model="formData.scopeImg" :limit="1" />
</el-form-item>
<el-form-item label="备注" prop="note">
<el-input v-model="formData.note" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button v-if="!isReadonly" type="primary" @click="handleSubmit()"> 保存 </el-button>
</div>
</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 { mockData } from './landData';
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 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({
gridName: '',
keyword: '',
regionCode: '',
id: '',
status: -1,
});
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: 'landTypeName' },
{ label: '土壤类型', prop: 'soilTypeName' },
{ label: '所属行政区域', prop: 'fullRegionName', width: 160 },
{ label: '所属网格', prop: 'gridName', width: 90 },
{ label: '具体位置', prop: 'address', width: 160 },
{ label: '土地承包经营人', prop: 'propertyName' },
{ label: '联系电话', prop: 'propertyPhone' },
{ label: '土地经营权证书编号', prop: 'landCode', width: 160 },
{ label: '是否土地流转', prop: 'isTransfer' },
{ label: '土地受让方', prop: 'transferName' },
{ label: '受让方联系电话', prop: 'transferPhone', width: 100 },
{ label: '流转合同', prop: 'transferContract' },
{ label: '信息填报人', prop: 'fillName' },
{ label: '信息填报单位', prop: 'fillGroup' },
{ label: '信息填报时间', prop: 'fillTime' },
// { label: '', prop: 'updateName' },
// { label: '', prop: 'updateGroup' },
{ label: '信息更新时间', prop: 'updateTime' },
],
actions: [
{
name: '查看',
icon: 'view',
event: ({ row }) => handleView(row),
},
{
name: '编辑',
icon: 'edit',
event: ({ row }) => handleEdit(row),
},
{
type: 'danger',
name: '删除',
icon: 'delete',
event: ({ row }) => handleDelete(row),
},
],
});
const handleTabChange = ({ name }) => {
// activeTab.value = name;
console.log('切换tab', 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 = (row) => {
isReadonly.value = true;
formData.value = { ...row };
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 createGrid(formData.value);
ElMessage.success('新增成功');
resetForm();
visible.value = false;
getData();
} else {
await updateGrid(formData.value);
ElMessage.success('更新成功');
resetForm();
visible.value = false;
getData();
}
} catch (error) {
ElMessage.error(error.message || '新增失败,请重试');
}
};
// ---------------------------------------------------------------------
//
// ---------------------------------------------------------------------
import { createGrid, updateGrid, deleteGrid, fetchGridList, getGridDetail, exportGrid } from '@/apis/landResourceManagement/gridManagement';
onMounted(() => {
getData();
});
const getData = async () => {
// const filteredParams = filterObject(searchForm.value);
// const response = await fetchGridList(filteredParams);
// crudData.value = Array.isArray(response.data.records) ? response.data.records : [];
loading.value = true;
//
await new Promise((resolve) => setTimeout(resolve, 300));
crudData.value = mockData
.filter((item) => item.status === activeTab.value)
.map((item) => ({
...item,
landTypeName: '林地',
}));
pageData.value.total = crudData.value.length;
loading.value = false;
};
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;
}
}
</style>

View File

@ -1,7 +0,0 @@
<template>
<LandCrud type="forest" />
</template>
<script setup>
import LandCrud from './components/LandCrud.vue';
</script>

View File

@ -31,7 +31,7 @@
@size-change="handleSizeChange" @size-change="handleSizeChange"
> >
<template #menu="scope"> <template #menu="scope">
<custom-table-operate :actions="crudOptions.actions" :data="scope" /> <custom-table-operate :actions="crudOptions.actions" :show="false" :data="scope" />
</template> </template>
</avue-crud> </avue-crud>
<el-dialog :key="dialogTitle" v-model="visible" :title="dialogTitle" width="60%" align-center :draggable="true"> <el-dialog :key="dialogTitle" v-model="visible" :title="dialogTitle" width="60%" align-center :draggable="true">

View File

@ -1,47 +1,458 @@
<template> <template>
<div class="mapDiv"> <div class="custom-page">
<tdt-map ref="tdtMapRef" :center="state.center" :zoom="state.zoom" :style="{ height: mapHeight }"> <!-- 左侧面板 -->
<!-- <tdt-tile-layer :layer-type="'img_w'"></tdt-tile-layer> --> <div class="panel-left">
<!-- <tdt-tile-layer :layer-type="'cia_w'" :z-index="2"></tdt-tile-layer> --> <el-input v-model="searchTerm" placeholder="搜索地块名称" clearable style="margin-bottom: 15px"></el-input>
<tdt-tilelayer :url="state.img_w_url" :z-index="1"></tdt-tilelayer>
<tdt-tilelayer :url="state.cia_w_url" :z-index="2"></tdt-tilelayer> <el-table :data="filteredFields" highlight-current-row style="width: 100%" @row-click="selectField">
</tdt-map> <el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="area" label="面积(㎡)">
<template #default="{ row }">
{{ row.area.toFixed(2) }}
</template>
</el-table-column>
</el-table>
<div class="stats">总面积: {{ totalArea.toFixed(2) }} </div>
<div class="button-group">
<el-button type="primary" @click="startDrawing">新增地块</el-button>
<el-button type="danger" :disabled="!selectedField" @click="removeField">删除地块</el-button>
</div>
</div>
<!-- 右侧地图区域 -->
<div class="panel-right">
<div ref="mapContainer" class="map"></div>
<div class="layer-switch">
<el-radio-group v-model="layerType" size="small" @change="changeBaseLayer">
<el-radio-button label="vector">矢量</el-radio-button>
<el-radio-button label="image">影像</el-radio-button>
</el-radio-group>
</div>
<div class="field-layer-switch">
<el-switch v-model="showFieldLayer" active-text="显示地块" @change="toggleFieldLayer"></el-switch>
</div>
<div class="zoom-scale">缩放级别: {{ mapZoom }}, 比例尺: 1:{{ scale.toLocaleString() }}</div>
<el-dialog v-model="drawDialogVisible" title="新增地块">
<el-form>
<el-form-item label="地块名称">
<el-input v-model="newFieldName"></el-input>
</el-form-item>
<el-form-item label="地块面积">
<el-input :value="newFieldArea.toFixed(2) + ' ㎡'" readonly></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cancelDrawing">取消</el-button>
<el-button type="primary" @click="saveNewField">保存</el-button>
</template>
</el-dialog>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive, onMounted, ref } from 'vue'; import { ref, computed, onMounted, watch } from 'vue';
import { TdtMap, TdtTileLayer } from 'vue-tianditu'; import { ElMessage } from 'element-plus';
import { map_config } from '@/config/map'; import { map_config } from '@/config/map';
import { loadTiandituApi } from '@/utils/mapLoader';
const key = map_config.tianditu.token; // 使localStorage
const fieldService = {
getFields() {
const fields = localStorage.getItem('fields');
return Promise.resolve(fields ? JSON.parse(fields) : []);
},
createField(field) {
return this.getFields().then((fields) => {
field.id = Date.now();
fields.push(field);
localStorage.setItem('fields', JSON.stringify(fields));
return field;
});
},
deleteField(id) {
return this.getFields().then((fields) => {
const newFields = fields.filter((f) => f.id !== id);
localStorage.setItem('fields', JSON.stringify(newFields));
return newFields;
});
},
};
const state = reactive({ const mapContainer = ref(null);
center: [100.088, 23.883], const mapObj = ref(null);
zoom: 14, const T = ref(null); // API
img_w_url: `http://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${key}`,
cia_w_url: `http://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${key}`, //
// URL const fields = ref([]);
// ... const selectedField = ref(null);
const searchTerm = ref('');
const newFieldName = ref('');
const drawDialogVisible = ref(false);
const newFieldCoords = ref([]);
const newFieldArea = ref(0);
const layerType = ref('image'); //
const showFieldLayer = ref(true);
const mapZoom = ref(12);
const scale = ref(0);
//
const baseLayers = {
vector: null,
vectorLabel: null,
image: null,
imageLabel: null,
};
//
const fieldPolygons = ref([]); //
const polygonTool = ref(null);
//
const filteredFields = computed(() => {
const term = searchTerm.value.toLowerCase();
return term ? fields.value.filter((f) => f.name.toLowerCase().includes(term)) : fields.value;
}); });
// const totalArea = computed(() => {
const mapHeight = ref('100vh'); return fields.value.reduce((sum, field) => sum + field.area, 0);
});
onMounted(() => { onMounted(() => {
console.log('地图组件已挂载'); loadFields();
// API loadTiandituApi(map_config.tianditu).then((tianditu) => {
if (window.T) { T.value = tianditu;
console.log('天地图API已加载:', window.T); initMap();
} else { });
console.error('天地图API未加载');
}
}); });
//
const initMap = () => {
if (!T.value) return;
//
const center = new T.value.LngLat(...map_config.tianditu.center);
mapObj.value = new T.value.Map(mapContainer.value, {
center: center,
zoom: map_config.tianditu.zoom,
minZoom: 3,
maxZoom: 18,
});
//
initLayers();
//
mapObj.value.addControl(new T.value.Control.Zoom());
mapObj.value.addControl(new T.value.Control.Scale());
//
mapObj.value.addEventListener('zoomend', updateScale);
mapObj.value.addEventListener('moveend', updateScale);
//
updateScale();
};
//
const initLayers = () => {
if (!T.value || !mapObj.value) return;
//
const vecLayer = new T.value.TileLayer(
`https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${map_config.tianditu.token}`,
{ minZoom: 3, maxZoom: 18, zIndex: 1 }
);
const cvaLayer = new T.value.TileLayer(
`https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${map_config.tianditu.token}`,
{ minZoom: 3, maxZoom: 18, zIndex: 2 }
);
//
const imgLayer = new T.value.TileLayer(
`https://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${map_config.tianditu.token}`,
{ minZoom: 3, maxZoom: 18, zIndex: 1 }
);
const ciaLayer = new T.value.TileLayer(
`https://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${map_config.tianditu.token}`,
{ minZoom: 3, maxZoom: 18, zIndex: 2 }
);
//
baseLayers.vector = vecLayer;
baseLayers.vectorLabel = cvaLayer;
baseLayers.image = imgLayer;
baseLayers.imageLabel = ciaLayer;
//
mapObj.value.addLayer(imgLayer);
mapObj.value.addLayer(ciaLayer);
};
//
const updateScale = () => {
if (mapObj.value) {
mapZoom.value = mapObj.value.getZoom();
scale.value = Math.round(mapObj.value.getScale());
}
};
//
const changeBaseLayer = () => {
if (!mapObj.value) return;
//
[baseLayers.vector, baseLayers.vectorLabel, baseLayers.image, baseLayers.imageLabel].forEach((layer) => {
mapObj.value.removeLayer(layer);
});
//
if (layerType.value === 'vector') {
mapObj.value.addLayer(baseLayers.vector);
mapObj.value.addLayer(baseLayers.vectorLabel);
} else {
mapObj.value.addLayer(baseLayers.image);
mapObj.value.addLayer(baseLayers.imageLabel);
}
};
//
const loadFields = () => {
fieldService.getFields().then((data) => {
fields.value = data;
refreshOverlays();
});
};
//
const refreshOverlays = () => {
if (!mapObj.value || !T.value) return;
//
clearFieldOverlays();
if (!showFieldLayer.value) return;
//
fields.value.forEach((field) => {
const polygon = createPolygon(field, selectedField.value?.id === field.id);
mapObj.value.addOverLay(polygon);
fieldPolygons.value.push(polygon);
});
};
//
const clearFieldOverlays = () => {
fieldPolygons.value.forEach((polygon) => {
mapObj.value.removeOverLay(polygon);
});
fieldPolygons.value = [];
};
//
const addFieldToMap = (field) => {
const polygon = createPolygon(field);
mapObj.value.addOverLay(polygon); //
return polygon;
};
//
const removeFieldFromMap = (polygon) => {
mapObj.value.removeOverLay(polygon); //
};
//
const clearAllOverlays = () => {
mapObj.value.clearOverLays(); //
};
//
const createPolygon = (field, isSelected = false) => {
const coords = field.geometry.coordinates[0].map((c) => new T.value.LngLat(c[0], c[1]));
return new T.value.Polygon(coords, {
color: isSelected ? '#ff0000' : '#1890ff',
weight: isSelected ? 3 : 2,
opacity: 0.8,
fillColor: isSelected ? '#ffa39e' : '#e6f7ff',
fillOpacity: 0.5,
});
};
//
const selectField = (field) => {
selectedField.value = field;
refreshOverlays();
if (mapObj.value && T.value) {
const coords = field.geometry.coordinates[0];
const center = coords
.reduce(
(acc, coord) => {
return [acc[0] + coord[0], acc[1] + coord[1]];
},
[0, 0]
)
.map((v) => v / coords.length);
mapObj.value.panTo(new T.value.LngLat(center[0], center[1]));
}
};
//
const startDrawing = () => {
if (!mapObj.value || !T.value) return;
//
polygonTool.value = new T.value.PolygonTool(mapObj.value, {
showLabel: true,
color: '#fa541c',
weight: 3,
opacity: 0.6,
fillColor: '#ffbb96',
fillOpacity: 0.4,
});
polygonTool.value.open();
polygonTool.value.addEventListener('draw', (e) => {
newFieldCoords.value = e.currentLnglats.map((lnglat) => [lnglat.lng, lnglat.lat]);
newFieldArea.value = e.currentArea;
drawDialogVisible.value = true;
polygonTool.value.close();
});
};
//
const saveNewField = () => {
if (!newFieldName.value.trim()) {
ElMessage.error('请输入地块名称');
return;
}
const newField = {
id: Date.now(),
name: newFieldName.value,
area: newFieldArea.value,
geometry: {
type: 'Polygon',
coordinates: [newFieldCoords.value],
},
};
fieldService.createField(newField).then(() => {
ElMessage.success('地块创建成功');
drawDialogVisible.value = false;
newFieldName.value = '';
loadFields();
});
};
//
const cancelDrawing = () => {
drawDialogVisible.value = false;
refreshOverlays();
};
//
const removeField = () => {
if (!selectedField.value) return;
fieldService.deleteField(selectedField.value.id).then(() => {
ElMessage.success('地块已删除');
selectedField.value = null;
loadFields();
});
};
//
const toggleFieldLayer = () => {
refreshOverlays();
};
</script> </script>
<style scoped> <style scoped>
.mapDiv { .custom-page {
display: flex;
padding: 0;
}
.panel-left {
width: 300px;
height: calc(100vh - 130px);
border-right: 1px solid #eee;
padding: 10px;
display: flex;
flex-direction: column;
background: #f8fafc;
}
.panel-right {
flex: 1;
position: relative;
}
.map {
width: 100%; width: 100%;
height: 100vh; /* 确保容器有明确高度 */ height: 100%;
}
.layer-switch {
position: absolute;
top: 15px;
right: 15px;
background: white;
padding: 8px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.field-layer-switch {
position: absolute;
top: 60px;
right: 15px;
background: white;
padding: 8px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
display: flex;
align-items: center;
gap: 8px;
}
.zoom-scale {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.85);
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.stats {
padding: 12px;
margin: 10px 0;
background: #edf2ff;
border-radius: 4px;
font-weight: 500;
color: #364fc7;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
.button-group > * {
flex: 1;
} }
</style> </style>

View File

@ -1,401 +0,0 @@
<template>
<div class="field-management">
<!-- 左侧面板 -->
<div class="panel left">
<el-input v-model="searchTerm" placeholder="搜索地块名称" clearable style="margin-bottom: 15px"></el-input>
<el-table :data="filteredFields" highlight-current-row style="width: 100%" height="calc(100vh - 300px)" @row-click="selectField">
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="area" label="面积(㎡)">
<template #default="{ row }">
{{ row.area.toFixed(2) }}
</template>
</el-table-column>
</el-table>
<div class="stats">总面积: {{ totalArea.toFixed(2) }} </div>
<div class="button-group">
<el-button type="primary" @click="startDrawing">新增地块</el-button>
<el-button type="danger" :disabled="!selectedField" @click="removeField">删除地块</el-button>
</div>
</div>
<!-- 右侧地图区域 -->
<div class="panel right">
<tdt-map class="map" :center="mapCenter" :zoom="mapZoom" @init="onMapInit"> </tdt-map>
<div class="layer-switch">
<el-radio-group v-model="layerType" size="small" @change="changeBaseLayer">
<el-radio-button label="vector">矢量</el-radio-button>
<el-radio-button label="image">影像</el-radio-button>
</el-radio-group>
</div>
<div class="field-layer-switch">
<el-switch v-model="showFieldLayer" active-text="显示地块"></el-switch>
</div>
<div class="zoom-scale">缩放级别: {{ mapZoom }}, 比例尺: 1:{{ scale.toLocaleString() }}</div>
<el-dialog v-model="drawDialogVisible" title="新增地块">
<el-form>
<el-form-item label="地块名称">
<el-input v-model="newFieldName"></el-input>
</el-form-item>
<el-form-item label="地块面积">
<el-input :value="newFieldArea.toFixed(2) + ' ㎡'" readonly></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cancelDrawing">取消</el-button>
<el-button type="primary" @click="saveNewField">保存</el-button>
</template>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { TdtMap } from 'vue-tianditu';
import { ElMessage } from 'element-plus';
// import { map_config } from '../../config/map';
// 使localStorage
const fieldService = {
getFields() {
const fields = localStorage.getItem('fields');
return Promise.resolve(fields ? JSON.parse(fields) : []);
},
createField(field) {
return this.getFields().then((fields) => {
field.id = Date.now();
fields.push(field);
localStorage.setItem('fields', JSON.stringify(fields));
return field;
});
},
deleteField(id) {
return this.getFields().then((fields) => {
const newFields = fields.filter((f) => f.id !== id);
localStorage.setItem('fields', JSON.stringify(newFields));
return newFields;
});
},
};
//
const fields = ref([]);
const selectedField = ref(null);
const searchTerm = ref('');
const newFieldName = ref('');
const drawDialogVisible = ref(false);
const newFieldCoords = ref([]);
const newFieldArea = ref(0);
const layerType = ref('vector');
const showFieldLayer = ref(true);
const map = ref(null);
const mapCenter = ref([100.088, 23.883]);
const mapZoom = ref(12);
const scale = ref(0);
//
const baseLayers = {
vector: null,
vectorLabel: null,
image: null,
imageLabel: null,
};
//
const overlayLayer = ref(null);
//
const filteredFields = computed(() => {
const term = searchTerm.value.toLowerCase();
return term ? fields.value.filter((f) => f.name.toLowerCase().includes(term)) : fields.value;
});
const totalArea = computed(() => {
return fields.value.reduce((sum, field) => sum + field.area, 0);
});
//
const onMapInit = (mapInstance) => {
map.value = mapInstance;
//
baseLayers.vector = new T.TileLayer('vec');
baseLayers.vectorLabel = new T.TileLayer('cva');
baseLayers.image = new T.TileLayer('img');
baseLayers.imageLabel = new T.TileLayer('cia');
//
map.value.addLayer(baseLayers.image);
map.value.addLayer(baseLayers.imageLabel);
//
overlayLayer.value = new T.Map.OverlayLayer();
map.value.addLayer(overlayLayer.value);
//
map.value.addControl(new T.Control.Zoom());
map.value.addControl(new T.Control.Scale());
//
map.value.addEventListener('zoomend', updateScale);
map.value.addEventListener('moveend', updateScale);
//
loadFields();
};
//
const updateScale = () => {
if (map.value) {
mapZoom.value = map.value.getZoom();
scale.value = Math.round(map.value.getScale());
}
};
//
const changeBaseLayer = () => {
if (!map.value) return;
//
map.value.removeLayer(baseLayers.vector);
map.value.removeLayer(baseLayers.vectorLabel);
map.value.removeLayer(baseLayers.image);
map.value.removeLayer(baseLayers.imageLabel);
//
if (layerType.value === 'vector') {
map.value.addLayer(baseLayers.vector);
map.value.addLayer(baseLayers.vectorLabel);
} else {
map.value.addLayer(baseLayers.image);
map.value.addLayer(baseLayers.imageLabel);
}
};
//
const loadFields = () => {
fieldService.getFields().then((data) => {
fields.value = data;
refreshOverlays();
});
};
//
const refreshOverlays = () => {
if (!map.value || !overlayLayer.value) return;
//
overlayLayer.value.clearOverLays();
//
if (!showFieldLayer.value) return;
//
fields.value.forEach((field) => {
const isSelected = selectedField.value?.id === field.id;
const polygon = createPolygon(field, isSelected);
overlayLayer.value.addOverLay(polygon);
});
};
//
const createPolygon = (field, isSelected = false) => {
const coords = field.geometry.coordinates[0].map((c) => new T.LngLat(c[0], c[1]));
return new T.Polygon(coords, {
color: isSelected ? '#ff0000' : '#1890ff',
weight: isSelected ? 3 : 2,
opacity: 0.8,
fillColor: isSelected ? '#ffa39e' : '#e6f7ff',
fillOpacity: 0.5,
});
};
//
const selectField = (field) => {
selectedField.value = field;
refreshOverlays();
//
if (map.value) {
const coords = field.geometry.coordinates[0];
const center = coords.reduce(
(acc, coord) => {
acc[0] += coord[0];
acc[1] += coord[1];
return acc;
},
[0, 0]
);
center[0] /= coords.length;
center[1] /= coords.length;
map.value.panTo(new T.LngLat(center[0], center[1]));
}
};
//
const startDrawing = () => {
if (!map.value) return;
//
refreshOverlays();
//
const polygonTool = new T.PolygonTool(map.value, {
showLabel: true,
color: '#fa541c',
weight: 3,
opacity: 0.6,
fillColor: '#ffbb96',
fillOpacity: 0.4,
});
polygonTool.open();
//
polygonTool.addEventListener('draw', (e) => {
newFieldCoords.value = e.currentLnglats.map((lnglat) => [lnglat.lng, lnglat.lat]);
newFieldArea.value = e.currentArea;
drawDialogVisible.value = true;
//
polygonTool.close();
});
};
//
const saveNewField = () => {
if (!newFieldName.value.trim()) {
ElMessage.error('请输入地块名称');
return;
}
const newField = {
id: Date.now(),
name: newFieldName.value,
area: newFieldArea.value,
geometry: {
type: 'Polygon',
coordinates: [newFieldCoords.value],
},
};
fieldService.createField(newField).then(() => {
ElMessage.success('地块创建成功');
drawDialogVisible.value = false;
newFieldName.value = '';
loadFields();
});
};
//
const cancelDrawing = () => {
drawDialogVisible.value = false;
refreshOverlays();
};
//
const removeField = () => {
if (!selectedField.value) return;
fieldService.deleteField(selectedField.value.id).then(() => {
ElMessage.success('地块已删除');
selectedField.value = null;
loadFields();
});
};
//
watch(showFieldLayer, refreshOverlays);
</script>
<style scoped>
.field-management {
display: flex;
height: calc(100vh - 160px);
}
.panel.left {
width: 320px;
padding: 15px;
border-right: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
background: #f8fafc;
}
.panel.right {
flex: 1;
position: relative;
}
.map {
width: 100%;
height: 100%;
}
.layer-switch {
position: absolute;
top: 15px;
right: 15px;
background: white;
padding: 8px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.field-layer-switch {
position: absolute;
top: 60px;
right: 15px;
background: white;
padding: 8px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
display: flex;
align-items: center;
gap: 8px;
}
.zoom-scale {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.85);
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.stats {
padding: 12px;
margin: 10px 0;
background: #edf2ff;
border-radius: 4px;
font-weight: 500;
color: #364fc7;
}
.button-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
.button-group > * {
flex: 1;
}
</style>

View File

@ -1,24 +1,31 @@
<template> <template>
<div class="land-search"> <div class="land-search">
<el-form :model="search" inline label-width="100px"> <div class="search-row">
<el-form-item label="地块名称"> <div class="search-item">
<el-input v-model="search.name" placeholder="请输入地块名称" clearable /> <span class="search-label">关键词搜索</span>
</el-form-item> <el-input v-model="localSearch.keyWord" placeholder="请输入关键词" clearable class="search-input" />
</div>
<el-form-item label="所属区域"> <div class="search-item">
<AreaCascader v-model:region-code="search.regionCode" v-model:grid-id="search.id" :width="300" /> <span class="search-label">地块名称</span>
</el-form-item> <el-input v-model="localSearch.name" placeholder="请输入地块名称" clearable class="search-input" />
</div>
<el-form-item> <div class="search-item">
<el-button type="primary" @click="onSearch">搜索</el-button> <span class="search-label">所属区域-网格</span>
<el-button @click="onReset">重置</el-button> <AreaCascader v-model:region-code="localSearch.regionCode" v-model:grid-id="localSearch.id" :width="500" :label="null" />
</el-form-item> </div>
</el-form>
<div class="search-item">
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { reactive, watch, toRefs } from 'vue'; import { ref } from 'vue';
const props = defineProps({ const props = defineProps({
search: { search: {
@ -27,33 +34,46 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['update:search', 'search', 'reset']); const emit = defineEmits(['on-search']);
const search = reactive({ ...props.search }); const localSearch = ref({ ...props.search });
watch( const handleSearch = () => {
search, emit('on-search', { ...localSearch.value });
(val) => {
emit('update:search', { ...val });
},
{ deep: true }
);
const onSearch = () => {
emit('search');
}; };
const onReset = () => { const handleReset = () => {
Object.keys(search).forEach((key) => { localSearch.value = {};
search[key] = ''; handleSearch();
});
emit('update:search', { ...search });
emit('reset');
}; };
</script> </script>
<style scoped> <style scoped>
.land-search { .land-search {
margin-bottom: 16px; padding: 16px;
background: #fff;
border-radius: 4px;
}
.search-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: center;
}
.search-item {
display: flex;
align-items: center;
}
.search-label {
font-size: 14px;
margin-right: 8px;
white-space: nowrap;
}
.search-input {
width: 200px;
} }
</style> </style>