Merge pull request 'refactor(sub-government-affairs-service): 重构新增网格' (#4) from 新增网格重构 into dev

Reviewed-on: #4
This commit is contained in:
shenhong 2025-06-13 09:20:26 +08:00
commit 2a7d36761d
8 changed files with 825 additions and 614 deletions

View File

@ -0,0 +1,43 @@
import request from '@/utils/axios';
// 新增POST
export function createGrid(data = {}) {
return request('/land-resource/gridManage/save', {
method: 'POST',
data,
});
}
// 删除DELETE
export function deleteGrid(id) {
return request(`/land-resource/gridManage/${id}`, {
method: 'DELETE',
});
}
// 修改PUT
export function updateGrid(data = {}) {
return request('//land-resource/gridManage/edit', {
method: 'PUT',
data,
});
}
// 查询列表GET
export function fetchGridList(params) {
return request('/land-resource/gridManage/page', {
method: 'GET',
params,
});
}
// 获取详情GET
export function getGridDetail(id) {
return request(`/land-resource/gridManage/${id}`, {
method: 'GET',
});
}
// 导出GET + Blob
export function exportGrid(params = {}) {
return request('/land-resource/gridManage/export', {
method: 'GET',
params,
responseType: 'blob',
});
}

View File

@ -1,40 +1,27 @@
<template> <template>
<div class="area-cascader-container" :style="{ width: width + 'px' }"> <div class="area-cascader-container" :style="containerStyle">
<!-- 一行显示模式 --> <!-- 一行显示模式 -->
<template v-if="!splitRows"> <template v-if="!splitRows">
<div v-if="label" class="area-cascader-label">{{ label }}</div> <div v-if="label" class="area-cascader-label">{{ label }}</div>
<div style="display: flex; gap: 8px; flex: 1"> <div class="controls">
<el-cascader <el-cascader v-model="regionModel" :options="areaOptions" :props="cascaderProps" :placeholder="areaPlaceholder" style="flex: 1" clearable />
v-model="selectedAreaCode"
:options="areaOptions"
:props="cascaderProps"
:placeholder="areaPlaceholder"
style="flex: 1"
clearable
/>
<span v-if="showSeparator" class="area-cascader-separator">{{ separator }}</span> <span v-if="showSeparator" class="area-cascader-separator">{{ separator }}</span>
<el-select v-model="selectedGridId" :placeholder="gridPlaceholder" style="flex: 1" clearable :disabled="!selectedAreaCode"> <el-select v-model="gridModel" :placeholder="gridPlaceholder" style="flex: 1" :disabled="!regionModel" clearable>
<el-option v-for="item in gridOptions" :key="item.gridName" :label="item.gridName" :value="item.id" /> <el-option v-for="item in gridOptions" :key="item.id" :label="item.gridName" :value="item.id" />
</el-select> </el-select>
</div> </div>
</template> </template>
<!-- 两行显示模式 --> <!-- 两行显示模式 -->
<template v-else> <template v-else>
<div class="area-item"> <div class="area-item">
<div class="area-cascader-label">所属行政区域</div> <div class="area-cascader-label">所属行政区域</div>
<el-cascader <el-cascader v-model="regionModel" :options="areaOptions" :props="cascaderProps" :placeholder="areaPlaceholder" clearable />
v-model="selectedAreaCode"
:options="areaOptions"
:props="cascaderProps"
:placeholder="areaPlaceholder"
style="flex: 1"
clearable
/>
</div> </div>
<div class="area-item"> <div class="area-item">
<div class="area-cascader-label">网格</div> <div class="area-cascader-label">网格</div>
<el-select v-model="selectedGridId" :placeholder="gridPlaceholder" style="flex: 1" clearable :disabled="!selectedAreaCode"> <el-select v-model="gridModel" :placeholder="gridPlaceholder" :disabled="!regionModel" clearable>
<el-option v-for="item in gridOptions" :key="item.gridName" :label="item.gridName" :value="item.id" /> <el-option v-for="item in gridOptions" :key="item.id" :label="item.gridName" :value="item.id" />
</el-select> </el-select>
</div> </div>
</template> </template>
@ -42,152 +29,49 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { ElCascader, ElSelect, ElOption } from 'element-plus';
import request from '@/utils/axios'; import request from '@/utils/axios';
import { useUserStore } from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
const props = defineProps({ const props = defineProps({
regionCode: { regionCode: { type: String, default: '' },
type: String, gridId: { type: [String, Number], default: '' },
default: '', label: { type: String, default: '行政区域-网格:' },
}, areaPlaceholder: { type: String, default: '请选择区域' },
gridId: { gridPlaceholder: { type: String, default: '请选择网格' },
type: [String, Number], width: { type: [Number, String], default: 500 },
default: '', showSeparator: { type: Boolean, default: false },
}, separator: { type: String, default: '-' },
label: { splitRows: { type: Boolean, default: false },
type: String,
default: '行政区域-网格:',
},
areaPlaceholder: {
type: String,
default: '请选择区域',
},
gridPlaceholder: {
type: String,
default: '请选择网格',
},
width: {
type: [Number, String],
default: 500,
},
showSeparator: {
type: Boolean,
default: false,
},
separator: {
type: String,
default: '-',
},
splitRows: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['update:regionCode', 'update:gridId']); const emit = defineEmits(['update:regionCode', 'update:gridId']);
const userStore = useUserStore(); const userStore = useUserStore();
const areaOptions = ref([]); const areaOptions = ref([]);
const gridOptions = ref([]); const gridOptions = ref([]);
// // computed regionModel & gridModel
const selectedAreaCode = ref(props.regionCode); const regionModel = computed({
const selectedGridId = ref(props.gridId); get() {
return props.regionCode;
//
const fetchAreaData = async () => {
try {
// 使 request params
const res = await request.get('/system/area/region', {
params: {
areaCode: '530000',
}, },
}); set(val) {
areaOptions.value = res.data ?? [];
} catch (err) {
console.error('区域数据加载失败', err);
}
};
//
const fetchGridList = async (regionCode) => {
if (!regionCode) return;
try {
// 使 request params
const res = await request.get('/land-resource/gridManage/page', {
params: {
regionCode: regionCode,
},
});
gridOptions.value = res.data?.records ?? [];
} catch (err) {
console.error('网格数据加载失败', err);
}
};
const selectedAreaLabel = computed(() => {
const findLabel = (options, code) => {
for (const item of options) {
if (item.areaCode === code) return item.areaName;
if (item.areaChildVOS?.length) {
const res = findLabel(item.areaChildVOS, code);
if (res) return res;
}
}
return '';
};
return findLabel(areaOptions.value, selectedAreaCode.value);
});
const selectedGridLabel = computed(() => {
const item = gridOptions.value.find((g) => g.id === selectedGridId.value);
return item?.gridName || '';
});
//
watch(
() => props.regionCode,
(val) => {
selectedAreaCode.value = val;
}
);
watch(
() => props.gridId,
(val) => {
selectedGridId.value = val;
}
);
//
watch(selectedAreaCode, (val) => {
emit('update:regionCode', val); emit('update:regionCode', val);
selectedGridId.value = ''; // gridId // reset grid when region changes
emit('update:gridId', ''); emit('update:gridId', '');
gridOptions.value = [];
fetchGridList(val); fetchGridList(val);
},
}); });
const gridModel = computed({
watch(selectedGridId, (val) => { get() {
return props.gridId;
},
set(val) {
emit('update:gridId', val); emit('update:gridId', val);
},
}); });
// v-model:value
// const updateValue = () => {
// console.log('update:value', selectedAreaCode.value, selectedGridName.value, selectedGridId.value);
// emit('update:value', {
// regionCode: selectedAreaCode.value,
// gridName: selectedGridName.value,
// gridId: selectedGridId.value,
// });
// };
onMounted(() => {
fetchAreaData();
});
//
const cascaderProps = computed(() => ({ const cascaderProps = computed(() => ({
label: 'areaName', label: 'areaName',
value: 'areaCode', value: 'areaCode',
@ -195,37 +79,61 @@ const cascaderProps = computed(() => ({
emitPath: false, emitPath: false,
expandTrigger: 'hover', expandTrigger: 'hover',
})); }));
async function fetchAreaData() {
try {
const res = await request.get('/system/area/region', { params: { areaCode: '530000' } });
areaOptions.value = res.data || [];
} catch (err) {
console.error('区域数据加载失败', err);
}
}
async function fetchGridList(regionCode) {
if (!regionCode) return;
try {
const res = await request.get('/land-resource/gridManage/page', { params: { regionCode } });
gridOptions.value = res.data?.records || [];
} catch (err) {
console.error('网格数据加载失败', err);
}
}
onMounted(fetchAreaData);
//
const containerStyle = computed(() => ({
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
display: 'flex',
gap: '18px',
flexDirection: props.splitRows ? 'column' : 'row',
}));
</script> </script>
<style scoped> <style scoped>
.area-cascader-container { .area-cascader-container {
display: flex;
gap: 18px;
margin: 0; margin: 0;
padding: 0; padding: 0;
/* 分行显示时垂直排列 */
flex-direction: v-bind('splitRows ? "column" : "row"');
} }
.area-cascader-label { .area-cascader-label {
font-size: 14px; font-size: 14px;
color: #606266; color: #606266;
text-align: right; text-align: right;
line-height: 32px; line-height: 32px;
box-sizing: border-box;
width: 120px; width: 120px;
padding-right: v-bind('splitRows ? "12px" : "0"');
} }
.controls {
.area-cascader-separator { display: flex;
align-self: center; gap: 8px;
font-size: 16px; flex: 1;
color: #606266;
margin: 0;
padding: 0;
} }
.area-item { .area-item {
display: flex; display: flex;
gap: 0; gap: 0;
align-items: center;
}
.area-cascader-separator {
align-self: center;
font-size: 16px;
color: #606266;
} }
</style> </style>

View File

@ -24,7 +24,7 @@ const props = defineProps({
default: false, default: false,
}, },
modelValue: { modelValue: {
type: Array, type: [Array, String],
default: () => [], default: () => [],
}, },
label: { label: {
@ -39,6 +39,10 @@ const props = defineProps({
type: [Number, String], type: [Number, String],
default: 500, default: 500,
}, },
emitPath: {
type: Boolean,
default: true,
},
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@ -46,14 +50,35 @@ const emit = defineEmits(['update:modelValue']);
const userStore = useUserStore(); const userStore = useUserStore();
const areaOptions = ref([]); const areaOptions = ref([]);
const selectedAreaPath = ref([...props.modelValue]);
//
// const selectedAreaPath = props.emitPath ? ref([].concat(props.modelValue)) : ref(props.modelValue);
const selectedAreaPath = computed({
get() {
// &
if (props.emitPath) {
return Array.isArray(props.modelValue) ? props.modelValue : [];
} else {
return typeof props.modelValue === 'string' ? props.modelValue : '';
}
},
set(val) {
//
if (props.emitPath) {
emit('update:modelValue', Array.isArray(val) ? val : []);
} else {
emit('update:modelValue', typeof val === 'string' ? val : '');
}
},
});
// //
const cascaderProps = computed(() => ({ const cascaderProps = computed(() => ({
label: 'areaName', label: 'areaName',
value: 'areaCode', value: 'areaCode',
children: 'areaChildVOS', children: 'areaChildVOS',
emitPath: true, emitPath: props.emitPath,
expandTrigger: 'hover', expandTrigger: 'hover',
})); }));
@ -70,7 +95,7 @@ const fetchAreaData = async () => {
console.error('加载行政区域失败', err); console.error('加载行政区域失败', err);
} }
}; };
// 使
// // => // // =>
// watch( // watch(
// () => props.modelValue, // () => props.modelValue,
@ -80,9 +105,19 @@ const fetchAreaData = async () => {
// ); // );
// => // =>
watch(selectedAreaPath, (val) => { watch(
emit('update:modelValue', val); selectedAreaPath,
}); (val) => {
if (props.emitPath) {
//
emit('update:modelValue', Array.isArray(val) ? val : []);
} else {
//
emit('update:modelValue', typeof val === 'string' ? val : '');
}
},
{ deep: true }
);
onMounted(() => { onMounted(() => {
fetchAreaData(); fetchAreaData();

View File

@ -12,6 +12,7 @@
:auto-upload="true" :auto-upload="true"
:disabled="readonly" :disabled="readonly"
:accept="accept" :accept="accept"
@preview="handlePreview"
> >
<el-icon v-if="fileList.length < limit"><Plus /></el-icon> <el-icon v-if="fileList.length < limit"><Plus /></el-icon>
</el-upload> </el-upload>
@ -24,78 +25,74 @@ import { ref, computed } from 'vue';
import { Plus } from '@element-plus/icons-vue'; import { Plus } from '@element-plus/icons-vue';
import { CommonUpload } from '@/apis/index'; import { CommonUpload } from '@/apis/index';
// 1. props & emit
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: { type: [Array, String], default: () => [] },
type: Array, ossUrl: { type: String, default: 'http://gov-cloud.oss-cn-chengdu.aliyuncs.com/' },
default: () => [], limit: { type: Number, default: 5 },
}, accept: { type: String, default: 'image/*' },
ossUrl: { readonly: { type: Boolean, default: false },
type: String,
default: 'http://gov-cloud.oss-cn-chengdu.aliyuncs.com/',
},
limit: {
type: Number,
default: 5,
},
accept: {
type: String,
default: 'image/*',
},
readonly: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
// el-upload file-list // 2. computed limit
const fileList = computed(() => { const selectedFiles = computed({
return props.modelValue.map((path, idx) => ({ get() {
name: `image_${idx}`, // limit===1 1
url: props.ossUrl + path, if (props.limit === 1 && typeof props.modelValue === 'string' && props.modelValue) {
uid: `${idx}`, return [props.modelValue];
})); }
//
return Array.isArray(props.modelValue) ? props.modelValue : [];
},
set(val) {
//
if (props.limit === 1) {
emit('update:modelValue', val.length ? val[0] : '');
} else {
emit('update:modelValue', val);
}
},
}); });
// // 3. fileList & previewList selectedFiles
const fileList = computed(() =>
selectedFiles.value.map((path, idx) => ({
name: `file_${idx}`,
url: props.ossUrl + path,
uid: `${idx}`,
}))
);
const previewShow = ref(false); const previewShow = ref(false);
const previewList = computed(() => fileList.value.map((item) => item.url));
const previewIndex = ref(0); const previewIndex = ref(0);
const previewList = computed(() => fileList.value.map((f) => f.url));
// // 4. &
const customUploadRequest = async ({ file, onSuccess, onError }) => { const customUploadRequest = async ({ file, onSuccess, onError }) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
try { try {
const res = await CommonUpload(formData); const res = await CommonUpload(formData);
// { code:200, data: { url: 'relative/path.jpg' } }
onSuccess(res, file); onSuccess(res, file);
} catch (err) { } catch (err) {
onError(err); onError(err);
} }
}; };
// modelValue
function handleUploadSuccess(res) { function handleUploadSuccess(res) {
const relative = res.data?.url; const relative = res.data?.url;
if (relative) { if (!relative) return;
const newArr = [...props.modelValue, relative]; //
emit('update:modelValue', newArr); selectedFiles.value = [...selectedFiles.value, relative];
}
} }
//
function handleRemove(file) { function handleRemove(file) {
const fullUrl = file.url; const rel = file.url.replace(props.ossUrl, '');
const relative = fullUrl.replace(props.ossUrl, ''); selectedFiles.value = selectedFiles.value.filter((p) => p !== rel);
const newArr = props.modelValue.filter((path) => path !== relative);
emit('update:modelValue', newArr);
} }
// // 5.
function handlePreview(file) { function handlePreview(file) {
const idx = fileList.value.findIndex((item) => item.uid === file.uid); const idx = fileList.value.findIndex((item) => item.uid === file.uid);
if (idx !== -1) { if (idx >= 0) {
previewIndex.value = idx; previewIndex.value = idx;
previewShow.value = true; previewShow.value = true;
} }

View File

@ -95,16 +95,7 @@
<el-input v-model="formDataProperty.landCode" placeholder="请输入产权编号" /> <el-input v-model="formDataProperty.landCode" placeholder="请输入产权编号" />
</el-form-item> </el-form-item>
<el-form-item label="产权证书" prop="propertyCertificateUrl"> <el-form-item label="产权证书" prop="propertyCertificateUrl">
<el-upload <FileUploader v-model="formDataProperty.propertyCertificateUrl" :limit="1" />
:http-request="customUploadRequest"
:on-success="(res, file) => handleUploadSuccess(res, file, 'property')"
multiple
:show-file-list="true"
>
<template #trigger>
<el-button type="primary">点击上传</el-button>
</template>
</el-upload>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-tab-pane> </el-tab-pane>
@ -147,7 +138,7 @@
<el-input v-model="viewEditFormData.address" /> <el-input v-model="viewEditFormData.address" />
</el-form-item> </el-form-item>
<el-form-item label="所属网格" prop="gridId"> <el-form-item label="所属网格" prop="gridId">
<AreaCascader v-model:grid-id="viewEditFormData.gridId" :width="500" label="" /> <AreaCascader v-model="viewEditFormData.regionCode" v-model:grid-id="viewEditFormData.gridId" :width="500" label="" />
</el-form-item> </el-form-item>
<el-form-item label="土壤类型" prop="soilTypeId"> <el-form-item label="土壤类型" prop="soilTypeId">
<url-select <url-select
@ -160,20 +151,7 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="土地照片" prop="landUrl"> <el-form-item label="土地照片" prop="landUrl">
<template v-if="viewEditFormData.landUrl"> <FileUploader v-model="viewEditFormData.landUrl" :limit="1" />
<img :src="`${ossUrl}${viewEditFormData.landUrl}`" alt="土地照片" class="preview-image" />
</template>
<el-upload
v-if="!isView"
:http-request="customUploadRequest"
:on-success="(res, file) => handleUploadSuccess(res, file, 'viewEditBasic')"
multiple
:show-file-list="true"
>
<template #trigger>
<el-button type="primary">点击上传</el-button>
</template>
</el-upload>
</el-form-item> </el-form-item>
<el-form-item label="土地范围" prop="scope"> <el-form-item label="土地范围" prop="scope">
<el-input v-model="viewEditFormData.scope" /> <el-input v-model="viewEditFormData.scope" />
@ -190,20 +168,7 @@
<el-input v-model="viewEditFormData.landCode" /> <el-input v-model="viewEditFormData.landCode" />
</el-form-item> </el-form-item>
<el-form-item label="产权证书" prop="propertyCertificateUrl"> <el-form-item label="产权证书" prop="propertyCertificateUrl">
<template v-if="viewEditFormData.propertyCertificateUrl"> <FileUploader v-model="viewEditFormData.propertyCertificateUrl" :limit="1" />
<img :src="`${ossUrl}${viewEditFormData.propertyCertificateUrl}`" alt="产权证书" class="preview-image" />
</template>
<el-upload
v-if="!isView"
:http-request="customUploadRequest"
:on-success="(res, file) => handleUploadSuccess(res, file, 'viewEditProperty')"
multiple
:show-file-list="true"
>
<template #trigger>
<el-button type="primary">点击上传</el-button>
</template>
</el-upload>
</el-form-item> </el-form-item>
</el-form> </el-form>

View File

@ -25,6 +25,7 @@
> >
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
</el-upload> </el-upload>
<!-- <FileUploader v-model="localForm.cooperativePhoto" :limit="1" /> -->
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -45,7 +46,7 @@
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="营业执照"> <el-form-item label="营业执照">
<el-upload <!-- <el-upload
action="#" action="#"
:http-request="customUploadRequest" :http-request="customUploadRequest"
:on-success="(res, file) => handleUploadSuccess(res, file, 'businessLicence')" :on-success="(res, file) => handleUploadSuccess(res, file, 'businessLicence')"
@ -56,7 +57,8 @@
:on-remove="() => handleRemove('businessLicence')" :on-remove="() => handleRemove('businessLicence')"
> >
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
</el-upload> </el-upload> -->
<FileUploader v-model="localForm.businessLicence" :limit="1" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>

View File

@ -2,391 +2,260 @@
<div class="custom-page"> <div class="custom-page">
<avue-crud <avue-crud
ref="crudRef" ref="crudRef"
v-model="state.form" v-model:page="pageData"
v-model:search="state.query" :data="crudData"
v-model:page="state.pageData" :option="crudOptions"
:table-loading="state.loading" :table-loading="loading"
:data="state.data" @refresh-change="handleRefresh"
:option="state.options" @current-change="handleCurrentChange"
@refresh-change="refreshChange" @size-change="handleSizeChange"
@search-reset="searchChange"
@search-change="searchChange"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
> >
<template #search="{ size }"> <template #search>
<AreaCascader v-model:value="state.query" placeholder="选择行政区域与网格" :width="400" /> <div class="custom-search">
</template> <AreaCascader v-model:region-code="searchForm.regionCode" v-model:grid-id="searchForm.id" :width="600" />
<el-button type="primary" @click="handleSearch"> 搜索 </el-button>
<template #menu-left> <el-button @click="resetSearch"> 重置 </el-button>
<el-button type="success" icon="download" @click="onExport">导出</el-button>
</template>
<template #menu="scope">
<custom-table-operate :actions="state.options.actions" :data="scope" />
</template>
<template #detail="scope">
<el-tabs type="border-card">
<el-tab-pane label="基本信息">
<avue-detail :option="baseDetailOption" :data="scope.row"></avue-detail>
</el-tab-pane>
<el-tab-pane label="网格地图">
<div v-if="scope.row.mapUrl" style="height: 400px">
<img :src="scope.row.mapUrl" style="max-width: 100%; max-height: 100%" />
</div> </div>
<el-empty v-else description="暂无地图数据"></el-empty> </template>
</el-tab-pane> <template #menu-left>
<el-tab-pane label="其他信息"> <el-button type="primary" icon="Plus" @click="handleAdd">新增网格</el-button>
<avue-detail :option="otherDetailOption" :data="scope.row"></avue-detail> <!-- <el-button type="success" icon="download" @click="handleExport">导出</el-button> -->
</el-tab-pane> </template>
</el-tabs> <template #menu="scope">
<custom-table-operate :actions="crudOptions.actions" :data="scope" />
</template> </template>
</avue-crud> </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> </div>
</template> </template>
<script setup>
import { reactive, ref } from 'vue';
import { useApp } from '@/hooks';
import { CRUD_OPTIONS } from '@/config';
import { isEmpty, downloadFile } from '@/utils';
import { useUserStore } from '@/store/modules/user';
import { compact } from 'lodash';
import { GetEntityList, AddEntity, UpdateEntity, DeleteEntity, ExportEntity } from '@/apis/resource/grid';
const { VITE_APP_BASE_API } = import.meta.env; <script setup>
const app = useApp(); // ---------------------------------------------------------------------
// 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';
const UserStore = useUserStore(); const UserStore = useUserStore();
const crudRef = ref(null); const user = UserStore.getUserInfo();
const state = reactive({ console.log('admin 属性:', user.admin);
loading: false,
query: { const loading = ref(false);
current: 1,
size: 10, const visible = ref(false);
const isReadonly = ref(false);
const dialogTitle = ref();
const formData = ref({
gridName: '', 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: '', regionCode: '',
}, id: '',
form: {}, status: -1,
selection: [], });
options: { 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, ...CRUD_OPTIONS,
addBtnText: '添加网格', addBtn: false,
// detail: true, searchBtn: false,
// detailTitle: '', emptyBtn: false,
column: [ column: [
{ { label: '网格编号', prop: 'id' },
label: '网格编号', { label: '网格名称', prop: 'gridName' },
prop: 'id', { label: '所属行政区域', prop: 'gridAreaName' },
addDisplay: false, { label: '备注', prop: 'note' },
},
{
label: '网格名称',
prop: 'gridName',
rules: {
required: true,
message: '请输入',
trigger: 'blur',
},
},
{
label: '所属行政区域',
prop: 'gridAreaName',
addDisplay: false,
viewDisplay: true,
rules: {
required: true,
message: '请输入',
trigger: 'blur',
},
},
{
label: '所属行政区域',
prop: 'cities',
type: 'cascader',
hide: true,
span: 24,
width: 300,
addDisplay: true,
editDisplay: true,
viewDisplay: false,
emitPath: false,
props: {
label: 'areaName',
value: 'areaCode',
children: 'areaChildVOS',
},
dicUrl: `${VITE_APP_BASE_API}/system/area/region?areaCode=530000`,
dicHeaders: {
authorization: UserStore.token,
},
dicFormatter: (res) => res.data ?? [],
rules: {
required: true,
message: '请选择',
trigger: 'blur',
},
},
{
label: '网格地图',
prop: 'mapUrl',
type: 'upload',
hide: true,
// action: `${VITE_APP_BASE_API}/system/file/upload`,
},
// {
// label: '',
// prop: 'gridName',
// type: 'select',
// addDisplay: false,
// hide: true,
// // search: true,
// searchLabelWidth: 100,
// dicUrl: `${VITE_APP_BASE_API}/land-resource/gridManage/page?regionCode={{key}}`,
// props: {
// label: 'gridName',
// value: 'gridName',
// },
// dicHeaders: {
// authorization: UserStore.token,
// },
// dicFormatter: (res) => res.data?.records,
// },
{
label: '备注',
prop: 'note',
type: 'textarea',
span: 24,
rows: 4,
overHidden: true,
// width: 200,
},
{
label: '创建时间',
prop: 'createTime',
width: 200,
hide: true,
display: false,
},
], ],
actions: [ actions: [
{ {
name: '查看', name: '查看',
icon: 'view', icon: 'view',
event: ({ row }) => rowView(row), event: ({ row }) => handleView(row),
}, },
{ {
name: '编辑', name: '编辑',
icon: 'edit', icon: 'edit',
event: ({ row }) => rowEdit(row), event: ({ row }) => handleEdit(row),
}, },
{ {
type: 'danger', type: 'danger',
name: '删除', name: '删除',
icon: 'delete', icon: 'delete',
event: ({ row }) => rowDel(row), event: ({ row }) => handleDelete(row),
}, },
], ],
},
pageData: {
total: 0,
currentPage: 1,
pageSize: 10,
},
data: [],
currentRow: {},
}); });
const baseDetailOption = { const handleRefresh = async () => {
column: [ searchForm.value = { ...initialSearchForm };
{ getData();
label: '网格名称',
prop: 'gridName',
},
{
label: '所属行政区域',
prop: 'gridAreaName',
},
{
label: '备注',
prop: 'note',
},
],
}; };
const handleCurrentChange = (val) => {
const otherDetailOption = { pageData.value.currentPage = val;
column: [
{
label: '创建时间',
prop: 'createTime',
},
// ...
],
}; };
const handleSizeChange = (val) => {
// pageData.value.pageSize = val;
const loadData = () => {
state.loading = true;
GetEntityList(state.query)
.then((res) => {
if (res.code === 200) {
const { current, size, total, records } = res.data;
state.data = records;
state.pageData = {
currentPage: current || 1,
pageSize: size || 10,
total: total,
};
}
})
.catch((err) => {
app.$message.error(err.msg);
state.data = [];
})
.finally(() => {
state.loading = false;
});
}; };
const handleView = (row) => {
loadData(); isReadonly.value = true;
formData.value = { ...row };
// dialogTitle.value = '查看网格';
const currentChange = (current) => { visible.value = true;
state.query.current = current;
loadData();
}; };
const handleEdit = (row) => {
// isReadonly.value = false;
const sizeChange = (size) => { formData.value = { ...row };
state.query.size = size; dialogTitle.value = '编辑网格';
loadData(); visible.value = true;
}; };
const handleDelete = async (row) => {
// try {
const searchChange = (params, done) => { await ElMessageBox.confirm('确认删除该网格吗?', '提示', {
if (done) done();
state.query = params;
state.query.current = 1;
loadData();
};
//
const refreshChange = () => {
loadData();
app.$message.success('刷新成功');
};
//
const selectionChange = (rows) => {
state.selection = rows;
};
//
const rowView = (row) => {
crudRef.value.rowView(row);
};
const setCity = (row) => {
if (!isEmpty(row.cities)) {
row.provinceCode = row?.cities[0] ?? null;
row.cityCode = row?.cities[1] ?? null;
row.gridAreaCode = row?.cities[4] ?? null;
row.townCode = row?.cities[3] ?? null;
row.village = row?.cities[4] ?? null;
// row.village = row?.cities.join(',');
}
};
//
const rowSave = (row, done, loading) => {
setCity(row);
row.gridAreaCode = row.cities;
AddEntity(row)
.then((res) => {
if (res.code === 200) {
app.$message.success('添加成功!');
done();
loadData();
}
})
.catch((err) => {
app.$message.error(err.msg);
})
.finally(() => {
loading();
});
};
//
const rowEdit = (row) => {
const village = !isEmpty(row.village) ? row.village : [];
row.cities = compact([row.provinceCode, row.cityCode, row.gridAreaCode ?? '', row.townCode ?? '', ...village]);
crudRef.value.rowEdit(row);
};
const rowUpdate = (row, index, done, loading) => {
setCity(row);
UpdateEntity(row)
.then((res) => {
if (res.code === 200) {
app.$message.success('更新成功!');
done();
loadData();
}
})
.catch((err) => {
app.$message.error(err.msg);
})
.finally(() => {
loading();
});
};
//
const rowDel = (row, index, done) => {
if (isEmpty(row)) return;
app
.$confirm(`删除后信息将不可查看,确认要删除吗?`, '确定删除', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
})
.then(() => {
DeleteEntity({ id: row.id })
.then((res) => {
if (res.code === 200) {
app.$message.success('删除成功!');
loadData();
}
})
.catch((err) => {
app.$message.error(err.msg);
}); });
})
.catch(() => {}); 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 () => {
const onExport = () => { console.log('提交表单:', formData.value);
if (isEmpty(state.data)) { try {
app.$message.error('当前暂时没有可供导出的数据!'); if (dialogTitle.value === '新增网格') {
return; await createGrid(formData.value);
ElMessage.success('新增成功');
} else {
await updateGrid(formData.value);
ElMessage.success('更新成功');
} }
state.loading = true; } catch (error) {
const fileName = '网格明细表'; ElMessage.error(error.message || '新增失败,请重试');
ExportEntity(state.query)
.then((res) => {
if (res.status === 200) {
downloadFile(res.data, `${fileName}.xlsx`, 'blob');
app.$message.success('导出成功!');
} }
}) };
.catch((err) => {
app.$message.error('导出失败!'); // ---------------------------------------------------------------------
}) //
.finally(() => { // ---------------------------------------------------------------------
state.loading = false; 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 : [];
};
const handleAdd = () => {
console.log('handleAdd');
resetForm();
isReadonly.value = false;
dialogTitle.value = '新增网格';
visible.value = true;
};
const handleSearch = () => {
getData();
};
const handleCancel = () => {
visible.value = false;
}; };
</script> </script>
<style scoped lang="scss">
: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;
margin-bottom: 16px;
.el-button {
margin-left: 12px;
}
}
</style>

View File

@ -0,0 +1,392 @@
<template>
<div class="custom-page">
<avue-crud
ref="crudRef"
v-model="state.form"
v-model:search="state.query"
v-model:page="state.pageData"
:table-loading="state.loading"
:data="state.data"
:option="state.options"
@refresh-change="refreshChange"
@search-reset="searchChange"
@search-change="searchChange"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@row-save="rowSave"
@row-update="rowUpdate"
@row-del="rowDel"
>
<template #search="{ size }">
<AreaCascader v-model:value="state.query" placeholder="选择行政区域与网格" :width="400" />
</template>
<template #menu-left>
<el-button type="success" icon="download" @click="onExport">导出</el-button>
</template>
<template #menu="scope">
<custom-table-operate :actions="state.options.actions" :data="scope" />
</template>
<template #detail="scope">
<el-tabs type="border-card">
<el-tab-pane label="基本信息">
<avue-detail :option="baseDetailOption" :data="scope.row"></avue-detail>
</el-tab-pane>
<el-tab-pane label="网格地图">
<div v-if="scope.row.mapUrl" style="height: 400px">
<img :src="scope.row.mapUrl" style="max-width: 100%; max-height: 100%" />
</div>
<el-empty v-else description="暂无地图数据"></el-empty>
</el-tab-pane>
<el-tab-pane label="其他信息">
<avue-detail :option="otherDetailOption" :data="scope.row"></avue-detail>
</el-tab-pane>
</el-tabs>
</template>
</avue-crud>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import { useApp } from '@/hooks';
import { CRUD_OPTIONS } from '@/config';
import { isEmpty, downloadFile } from '@/utils';
import { useUserStore } from '@/store/modules/user';
import { compact } from 'lodash';
import { GetEntityList, AddEntity, UpdateEntity, DeleteEntity, ExportEntity } from '@/apis/resource/grid';
const { VITE_APP_BASE_API } = import.meta.env;
const app = useApp();
const UserStore = useUserStore();
const crudRef = ref(null);
const state = reactive({
loading: false,
query: {
current: 1,
size: 10,
gridName: '',
regionCode: '',
},
form: {},
selection: [],
options: {
...CRUD_OPTIONS,
addBtnText: '添加网格',
// detail: true,
// detailTitle: '',
column: [
{
label: '网格编号',
prop: 'id',
addDisplay: false,
},
{
label: '网格名称',
prop: 'gridName',
rules: {
required: true,
message: '请输入',
trigger: 'blur',
},
},
{
label: '所属行政区域',
prop: 'gridAreaName',
addDisplay: false,
viewDisplay: true,
rules: {
required: true,
message: '请输入',
trigger: 'blur',
},
},
{
label: '所属行政区域',
prop: 'cities',
type: 'cascader',
hide: true,
span: 24,
width: 300,
addDisplay: true,
editDisplay: true,
viewDisplay: false,
emitPath: false,
props: {
label: 'areaName',
value: 'areaCode',
children: 'areaChildVOS',
},
dicUrl: `${VITE_APP_BASE_API}/system/area/region?areaCode=530000`,
dicHeaders: {
authorization: UserStore.token,
},
dicFormatter: (res) => res.data ?? [],
rules: {
required: true,
message: '请选择',
trigger: 'blur',
},
},
{
label: '网格地图',
prop: 'mapUrl',
type: 'upload',
hide: true,
// action: `${VITE_APP_BASE_API}/system/file/upload`,
},
// {
// label: '',
// prop: 'gridName',
// type: 'select',
// addDisplay: false,
// hide: true,
// // search: true,
// searchLabelWidth: 100,
// dicUrl: `${VITE_APP_BASE_API}/land-resource/gridManage/page?regionCode={{key}}`,
// props: {
// label: 'gridName',
// value: 'gridName',
// },
// dicHeaders: {
// authorization: UserStore.token,
// },
// dicFormatter: (res) => res.data?.records,
// },
{
label: '备注',
prop: 'note',
type: 'textarea',
span: 24,
rows: 4,
overHidden: true,
// width: 200,
},
{
label: '创建时间',
prop: 'createTime',
width: 200,
hide: true,
display: false,
},
],
actions: [
{
name: '查看',
icon: 'view',
event: ({ row }) => rowView(row),
},
{
name: '编辑',
icon: 'edit',
event: ({ row }) => rowEdit(row),
},
{
type: 'danger',
name: '删除',
icon: 'delete',
event: ({ row }) => rowDel(row),
},
],
},
pageData: {
total: 0,
currentPage: 1,
pageSize: 10,
},
data: [],
currentRow: {},
});
const baseDetailOption = {
column: [
{
label: '网格名称',
prop: 'gridName',
},
{
label: '所属行政区域',
prop: 'gridAreaName',
},
{
label: '备注',
prop: 'note',
},
],
};
const otherDetailOption = {
column: [
{
label: '创建时间',
prop: 'createTime',
},
// ...
],
};
//
const loadData = () => {
state.loading = true;
GetEntityList(state.query)
.then((res) => {
if (res.code === 200) {
const { current, size, total, records } = res.data;
state.data = records;
state.pageData = {
currentPage: current || 1,
pageSize: size || 10,
total: total,
};
}
})
.catch((err) => {
app.$message.error(err.msg);
state.data = [];
})
.finally(() => {
state.loading = false;
});
};
loadData();
//
const currentChange = (current) => {
state.query.current = current;
loadData();
};
//
const sizeChange = (size) => {
state.query.size = size;
loadData();
};
//
const searchChange = (params, done) => {
if (done) done();
state.query = params;
state.query.current = 1;
loadData();
};
//
const refreshChange = () => {
loadData();
app.$message.success('刷新成功');
};
//
const selectionChange = (rows) => {
state.selection = rows;
};
//
const rowView = (row) => {
crudRef.value.rowView(row);
};
const setCity = (row) => {
if (!isEmpty(row.cities)) {
row.provinceCode = row?.cities[0] ?? null;
row.cityCode = row?.cities[1] ?? null;
row.gridAreaCode = row?.cities[4] ?? null;
row.townCode = row?.cities[3] ?? null;
row.village = row?.cities[4] ?? null;
// row.village = row?.cities.join(',');
}
};
//
const rowSave = (row, done, loading) => {
setCity(row);
row.gridAreaCode = row.cities;
AddEntity(row)
.then((res) => {
if (res.code === 200) {
app.$message.success('添加成功!');
done();
loadData();
}
})
.catch((err) => {
app.$message.error(err.msg);
})
.finally(() => {
loading();
});
};
//
const rowEdit = (row) => {
const village = !isEmpty(row.village) ? row.village : [];
row.cities = compact([row.provinceCode, row.cityCode, row.gridAreaCode ?? '', row.townCode ?? '', ...village]);
crudRef.value.rowEdit(row);
};
const rowUpdate = (row, index, done, loading) => {
setCity(row);
UpdateEntity(row)
.then((res) => {
if (res.code === 200) {
app.$message.success('更新成功!');
done();
loadData();
}
})
.catch((err) => {
app.$message.error(err.msg);
})
.finally(() => {
loading();
});
};
//
const rowDel = (row, index, done) => {
if (isEmpty(row)) return;
app
.$confirm(`删除后信息将不可查看,确认要删除吗?`, '确定删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
DeleteEntity({ id: row.id })
.then((res) => {
if (res.code === 200) {
app.$message.success('删除成功!');
loadData();
}
})
.catch((err) => {
app.$message.error(err.msg);
});
})
.catch(() => {});
};
//
const onExport = () => {
if (isEmpty(state.data)) {
app.$message.error('当前暂时没有可供导出的数据!');
return;
}
state.loading = true;
const fileName = '网格明细表';
ExportEntity(state.query)
.then((res) => {
if (res.status === 200) {
downloadFile(res.data, `${fileName}.xlsx`, 'blob');
app.$message.success('导出成功!');
}
})
.catch((err) => {
app.$message.error('导出失败!');
})
.finally(() => {
state.loading = false;
});
};
</script>