Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ec4b2d818 | |||
| 273772d68a |
@@ -1 +0,0 @@
|
||||
npx --no-install commitlint --edit "$1"
|
||||
1
.husky/commit-msg.bak
Normal file
1
.husky/commit-msg.bak
Normal file
@@ -0,0 +1 @@
|
||||
npx --no-install commitlint --edit "$1"
|
||||
4
env/.env
vendored
4
env/.env
vendored
@@ -10,8 +10,8 @@ VITE_WX_APPID = 'wx1a832d51073d3a35'
|
||||
VITE_APP_PUBLIC_BASE=/
|
||||
|
||||
# 后台请求地址
|
||||
VITE_SERVER_BASEURL = 'http://localhost:48080/admin-api'
|
||||
VITE_UPLOAD_BASEURL = 'http://localhost:48080/upload'
|
||||
VITE_SERVER_BASEURL = 'http://192.168.1.107:48080/admin-api'
|
||||
VITE_UPLOAD_BASEURL = 'http://192.168.1.107:48080/upload'
|
||||
# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
|
||||
|
||||
# 注意,如果是微信小程序,还有一套请求地址的配置,根据 develop、trial、release 分别设置上传地址,见 `src/utils/index.ts`。
|
||||
|
||||
2
env/.env.development
vendored
2
env/.env.development
vendored
@@ -6,4 +6,4 @@ VITE_DELETE_CONSOLE = false
|
||||
VITE_SHOW_SOURCEMAP = false
|
||||
|
||||
# 后台请求地址
|
||||
# VITE_SERVER_BASEURL = 'http://localhost:48080'
|
||||
# VITE_SERVER_BASEURL = 'http://1921.168.1.107:48080'
|
||||
|
||||
24
src/api/agri/batch.ts
Normal file
24
src/api/agri/batch.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
export interface AgriBatchVO {
|
||||
id: number
|
||||
batchNo: string
|
||||
plotId: number
|
||||
plotName: string
|
||||
cropId: number
|
||||
cropName: string
|
||||
farmerId: number
|
||||
farmerName: string
|
||||
varietyName: string
|
||||
sowingDate?: string
|
||||
transplantDate?: string
|
||||
expectedHarvestDate?: string
|
||||
actualHarvestDate?: string
|
||||
status?: number
|
||||
totalHarvestWeight?: number
|
||||
}
|
||||
|
||||
/** 获取批次简单列表(用于下拉选择) */
|
||||
export function getBatchSimpleList() {
|
||||
return http.get<AgriBatchVO[]>('/agri/batch/simple-list')
|
||||
}
|
||||
84
src/api/agri/operation.ts
Normal file
84
src/api/agri/operation.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 操作类型枚举 */
|
||||
export const OPERATION_TYPE_OPTIONS = [
|
||||
{ label: '播种', value: 0 },
|
||||
{ label: '浇水', value: 1 },
|
||||
{ label: '施肥', value: 2 },
|
||||
{ label: '除草', value: 3 },
|
||||
{ label: '打药', value: 4 },
|
||||
{ label: '其他', value: 5 },
|
||||
]
|
||||
|
||||
/** 操作类型映射 */
|
||||
export const OPERATION_TYPE_MAP: Record<number, string> = {
|
||||
0: '播种',
|
||||
1: '浇水',
|
||||
2: '施肥',
|
||||
3: '除草',
|
||||
4: '打药',
|
||||
5: '其他',
|
||||
}
|
||||
|
||||
export interface AgriOperationVO {
|
||||
id: number
|
||||
batchId: number
|
||||
batchNo: string
|
||||
cropName: string
|
||||
operationType: number
|
||||
operationDate: string
|
||||
operatorName?: string
|
||||
workContent?: string
|
||||
imageUrls?: string
|
||||
remark?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
export interface AgriOperationPageReqVO extends PageParam {
|
||||
batchId?: number
|
||||
operationType?: number
|
||||
}
|
||||
|
||||
export interface AgriOperationFormReqVO {
|
||||
id?: number
|
||||
batchId: number
|
||||
operationType: number
|
||||
operationDate: string
|
||||
operatorName?: string
|
||||
workContent?: string
|
||||
imageUrls?: string
|
||||
remark?: string
|
||||
longitude?: number
|
||||
latitude?: number
|
||||
}
|
||||
|
||||
/** 获取农事操作分页列表 */
|
||||
export function getOperationPage(params: AgriOperationPageReqVO) {
|
||||
return http.get<PageResult<AgriOperationVO>>('/agri/operation/page', params)
|
||||
}
|
||||
|
||||
/** 获取农事操作详情 */
|
||||
export function getOperation(id: number) {
|
||||
return http.get<AgriOperationVO>(`/agri/operation/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 创建农事操作 */
|
||||
export function createOperation(data: AgriOperationFormReqVO) {
|
||||
return http.post<number>('/agri/operation/create', data)
|
||||
}
|
||||
|
||||
/** 更新农事操作 */
|
||||
export function updateOperation(data: AgriOperationFormReqVO) {
|
||||
return http.put<void>('/agri/operation/update', data)
|
||||
}
|
||||
|
||||
/** 删除农事操作 */
|
||||
export function deleteOperation(id: number) {
|
||||
return http.delete<void>(`/agri/operation/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 导出农事操作 */
|
||||
export function exportOperation(params: AgriOperationPageReqVO) {
|
||||
return http.get<any>('/agri/operation/export', params, { responseType: 'blob' })
|
||||
}
|
||||
18
src/api/agri/plot.ts
Normal file
18
src/api/agri/plot.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { http } from '@/http/http'
|
||||
|
||||
export interface AgriPlotVO {
|
||||
id: number
|
||||
plotCode?: string
|
||||
plotName?: string
|
||||
area?: number
|
||||
boundaryGeojson?: string
|
||||
centerLongitude?: number
|
||||
centerLatitude?: number
|
||||
managerName?: string
|
||||
status?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export function getPlot(id: number) {
|
||||
return http.get<AgriPlotVO>(`/agri/plot/get?id=${id}`)
|
||||
}
|
||||
697
src/pages-agri/operation/form/index.vue
Normal file
697
src/pages-agri/operation/form/index.vue
Normal file
@@ -0,0 +1,697 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="pageTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="pb-180rpx">
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group title="农事信息" border>
|
||||
<!-- 批次选择 -->
|
||||
<wd-cell title="批次" title-width="180rpx" center>
|
||||
<wd-picker
|
||||
v-model="formData.batchId"
|
||||
:columns="batchColumns"
|
||||
label=""
|
||||
placeholder="请选择批次"
|
||||
@confirm="onBatchConfirm"
|
||||
/>
|
||||
</wd-cell>
|
||||
<!-- 操作日期 -->
|
||||
<wd-cell title="操作日期" title-width="180rpx" center>
|
||||
<wd-datetime-picker
|
||||
v-model="operationDateTimestamp"
|
||||
type="date"
|
||||
label=""
|
||||
placeholder="请选择操作日期"
|
||||
/>
|
||||
</wd-cell>
|
||||
<!-- 操作类型 -->
|
||||
<wd-cell title="操作类型" title-width="180rpx" center>
|
||||
<wd-picker
|
||||
v-model="formData.operationType"
|
||||
:columns="typeColumns"
|
||||
label=""
|
||||
placeholder="请选择类型"
|
||||
@confirm="onTypeConfirm"
|
||||
/>
|
||||
</wd-cell>
|
||||
<!-- 操作人 -->
|
||||
<wd-input
|
||||
v-model="formData.operatorName"
|
||||
label="操作人"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入操作人姓名"
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-cell-group title="工作详情" border>
|
||||
<!-- 工作内容 -->
|
||||
<wd-textarea
|
||||
v-model="formData.workContent"
|
||||
label="工作内容"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入工作内容"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
<!-- 备注 -->
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入备注"
|
||||
:maxlength="200"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-cell-group title="图片上传" border>
|
||||
<view class="px-24rpx py-16rpx">
|
||||
<wd-upload
|
||||
v-model:file-list="fileList"
|
||||
:upload-method="customUpload"
|
||||
accept="image"
|
||||
multiple
|
||||
:limit="9"
|
||||
@change="onUploadChange"
|
||||
/>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
|
||||
<wd-cell-group title="地块地图" border>
|
||||
<view class="map-section">
|
||||
<view class="map-status-row">
|
||||
<text class="map-status-text" :class="mapStatusClass">{{ mapStatusText }}</text>
|
||||
<wd-button size="small" plain @click="handleRelocate">定位当前位置</wd-button>
|
||||
</view>
|
||||
<map
|
||||
class="plot-map"
|
||||
:longitude="mapCenter.longitude"
|
||||
:latitude="mapCenter.latitude"
|
||||
:scale="mapScale"
|
||||
:markers="mapMarkers"
|
||||
:polygons="mapPolygons"
|
||||
:show-location="true"
|
||||
:enable-zoom="true"
|
||||
:enable-scroll="true"
|
||||
/>
|
||||
</view>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { UploadFile, UploadMethod } from 'wot-design-uni/components/wd-upload/types'
|
||||
import { OPERATION_TYPE_MAP } from '@/api/agri/operation'
|
||||
import type { AgriBatchVO } from '@/api/agri/batch'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getEnvBaseUrl, navigateBackPlus } from '@/utils'
|
||||
import { getGeoJsonPolygonPaths, isPointInGeoJson } from '@/utils/geo'
|
||||
import { getPlot } from '@/api/agri/plot'
|
||||
import {
|
||||
createOperation,
|
||||
getOperation,
|
||||
updateOperation,
|
||||
} from '@/api/agri/operation'
|
||||
import { getBatchSimpleList } from '@/api/agri/batch'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | string
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const pageTitle = computed(() => currentId.value ? '编辑农事记录' : '新增农事记录')
|
||||
const formLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const batchList = ref<AgriBatchVO[]>([])
|
||||
|
||||
const currentId = computed<number | undefined>(() => {
|
||||
if (props.id === undefined || props.id === null || props.id === '') {
|
||||
return undefined
|
||||
}
|
||||
const parsedId = Number(props.id)
|
||||
return Number.isFinite(parsedId) ? parsedId : undefined
|
||||
})
|
||||
|
||||
function createDefaultFormData() {
|
||||
return {
|
||||
id: undefined as number | undefined,
|
||||
batchId: undefined as number | undefined,
|
||||
operationType: undefined as number | undefined,
|
||||
operatorName: '',
|
||||
workContent: '',
|
||||
remark: '',
|
||||
}
|
||||
}
|
||||
|
||||
/** 操作日期(datetime-picker 绑定时间戳) */
|
||||
const operationDateTimestamp = ref<number>(Date.now())
|
||||
|
||||
/** 表单数据 */
|
||||
const formData = ref(createDefaultFormData())
|
||||
|
||||
/** 图片列表 */
|
||||
const fileList = ref<UploadFile[]>([])
|
||||
const locationPoint = ref<{ longitude: number, latitude: number } | null>(null)
|
||||
const mapScale = ref(16)
|
||||
const mapCenter = ref({ longitude: 116.397428, latitude: 39.90923 })
|
||||
const mapPolygons = ref<any[]>([])
|
||||
const currentPlotGeoJson = ref<any | null>(null)
|
||||
const mapReady = ref(false)
|
||||
const locationLoading = ref(false)
|
||||
const locationWatchActive = ref(false)
|
||||
const locationDenied = ref(false)
|
||||
let locationWatchTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const formRules = {
|
||||
batchId: [{ required: true, message: '请选择批次' }],
|
||||
operationType: [{ required: true, message: '请选择操作类型' }],
|
||||
}
|
||||
|
||||
const mapMarkers = computed(() => {
|
||||
if (!locationPoint.value) {
|
||||
return []
|
||||
}
|
||||
return [{
|
||||
id: 1,
|
||||
longitude: locationPoint.value.longitude,
|
||||
latitude: locationPoint.value.latitude,
|
||||
width: 28,
|
||||
height: 28,
|
||||
title: '当前位置',
|
||||
}]
|
||||
})
|
||||
|
||||
const inPlotRange = computed(() => {
|
||||
if (!locationPoint.value || !currentPlotGeoJson.value) {
|
||||
return false
|
||||
}
|
||||
return isPointInGeoJson(locationPoint.value, currentPlotGeoJson.value)
|
||||
})
|
||||
|
||||
const mapStatusText = computed(() => {
|
||||
if (locationLoading.value) {
|
||||
return '正在获取当前位置...'
|
||||
}
|
||||
if (!formData.value.batchId) {
|
||||
return '请选择批次后查看地块范围'
|
||||
}
|
||||
if (!currentPlotGeoJson.value) {
|
||||
return '当前批次暂无可展示的地块范围'
|
||||
}
|
||||
if (!locationPoint.value || locationDenied.value) {
|
||||
return '需要开启定位权限,用于显示当前位置和判断是否在地块范围内'
|
||||
}
|
||||
return inPlotRange.value ? '当前位于地块范围内' : '当前位置不在地块范围内'
|
||||
})
|
||||
|
||||
const mapStatusClass = computed(() => {
|
||||
if (!locationPoint.value || !currentPlotGeoJson.value || locationLoading.value) {
|
||||
return 'is-neutral'
|
||||
}
|
||||
return inPlotRange.value ? 'is-success' : 'is-warning'
|
||||
})
|
||||
|
||||
/** 批次下拉列 */
|
||||
const batchColumns = computed(() =>
|
||||
batchList.value
|
||||
.filter(batch => batch?.id !== undefined && batch?.id !== null)
|
||||
.map(batch => ({ value: batch.id, label: `${batch.batchNo} - ${batch.cropName}` })),
|
||||
)
|
||||
|
||||
/** 操作类型下拉列 */
|
||||
const typeColumns = computed(() =>
|
||||
Object.entries(OPERATION_TYPE_MAP).map(([value, label]) => ({ value: Number(value), label })),
|
||||
)
|
||||
|
||||
function normalizePickerValue(value: unknown): number | undefined {
|
||||
const rawValue = Array.isArray(value) ? value[0] : value
|
||||
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
||||
return undefined
|
||||
}
|
||||
const normalizedValue = Number(rawValue)
|
||||
return Number.isFinite(normalizedValue) ? normalizedValue : undefined
|
||||
}
|
||||
|
||||
function normalizeDateTimestamp(value?: string) {
|
||||
if (!value) {
|
||||
return Date.now()
|
||||
}
|
||||
const timestamp = new Date(value).getTime()
|
||||
return Number.isFinite(timestamp) ? timestamp : Date.now()
|
||||
}
|
||||
|
||||
function buildUploadFileList(imageUrls?: string) {
|
||||
if (!imageUrls) {
|
||||
return []
|
||||
}
|
||||
return imageUrls
|
||||
.split(',')
|
||||
.map(url => url.trim())
|
||||
.filter(Boolean)
|
||||
.map((url, index) => ({
|
||||
status: 'success' as const,
|
||||
url,
|
||||
uid: String(index),
|
||||
isImage: true,
|
||||
response: JSON.stringify({ data: url }),
|
||||
}))
|
||||
}
|
||||
|
||||
function getSelectedPlotId() {
|
||||
const selectedBatch = batchList.value.find(batch => batch.id === formData.value.batchId)
|
||||
return selectedBatch?.plotId
|
||||
}
|
||||
|
||||
function parseBoundaryGeoJson(boundaryGeojson?: string) {
|
||||
if (!boundaryGeojson) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return JSON.parse(boundaryGeojson)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function updateMapCenter(longitude: number, latitude: number) {
|
||||
mapCenter.value = { longitude, latitude }
|
||||
}
|
||||
|
||||
function applyPlotGeoJsonToMap(geoJson: any) {
|
||||
currentPlotGeoJson.value = geoJson
|
||||
const polygonPaths = getGeoJsonPolygonPaths(geoJson)
|
||||
mapPolygons.value = polygonPaths.map((points, index) => ({
|
||||
id: index + 1,
|
||||
points,
|
||||
strokeWidth: 2,
|
||||
strokeColor: '#1E80FF',
|
||||
fillColor: 'rgba(30,128,255,0.20)',
|
||||
}))
|
||||
|
||||
if (!locationPoint.value && polygonPaths[0]?.[0]) {
|
||||
updateMapCenter(polygonPaths[0][0].longitude, polygonPaths[0][0].latitude)
|
||||
}
|
||||
}
|
||||
|
||||
function clearPlotMap() {
|
||||
currentPlotGeoJson.value = null
|
||||
mapPolygons.value = []
|
||||
}
|
||||
|
||||
async function loadSelectedPlotMap() {
|
||||
const plotId = getSelectedPlotId()
|
||||
if (!plotId) {
|
||||
clearPlotMap()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const plot = await getPlot(plotId)
|
||||
const geoJson = parseBoundaryGeoJson(plot?.boundaryGeojson)
|
||||
if (!geoJson) {
|
||||
clearPlotMap()
|
||||
return
|
||||
}
|
||||
applyPlotGeoJsonToMap(geoJson)
|
||||
} catch {
|
||||
clearPlotMap()
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentLocation() {
|
||||
return new Promise<{ longitude: number, latitude: number }>((resolve, reject) => {
|
||||
uni.getLocation({
|
||||
type: 'gcj02',
|
||||
success: (res) => {
|
||||
resolve({
|
||||
longitude: Number(res.longitude),
|
||||
latitude: Number(res.latitude),
|
||||
})
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function openLocationPermissionSetting() {
|
||||
uni.showModal({
|
||||
title: '定位权限提醒',
|
||||
content: '需要开启定位权限,用于显示当前位置和判断是否在地块范围内',
|
||||
confirmText: '去开启',
|
||||
success: (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
if (typeof uni.openSetting === 'function') {
|
||||
uni.openSetting({})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleLocationFailure(showPermissionToast: boolean) {
|
||||
locationPoint.value = null
|
||||
locationDenied.value = true
|
||||
if (!showPermissionToast) {
|
||||
return
|
||||
}
|
||||
openLocationPermissionSetting()
|
||||
}
|
||||
|
||||
async function refreshCurrentLocation(showPermissionToast = true) {
|
||||
locationLoading.value = true
|
||||
try {
|
||||
const currentLocation = await getCurrentLocation()
|
||||
locationPoint.value = currentLocation
|
||||
locationDenied.value = false
|
||||
updateMapCenter(currentLocation.longitude, currentLocation.latitude)
|
||||
mapReady.value = true
|
||||
return currentLocation
|
||||
} catch {
|
||||
handleLocationFailure(showPermissionToast)
|
||||
return null
|
||||
} finally {
|
||||
locationLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRelocate() {
|
||||
await refreshCurrentLocation(true)
|
||||
}
|
||||
|
||||
function stopLocationWatch() {
|
||||
if (locationWatchTimer) {
|
||||
clearInterval(locationWatchTimer)
|
||||
locationWatchTimer = null
|
||||
}
|
||||
locationWatchActive.value = false
|
||||
}
|
||||
|
||||
function startLocationWatch() {
|
||||
stopLocationWatch()
|
||||
if (currentId.value) {
|
||||
return
|
||||
}
|
||||
locationWatchActive.value = true
|
||||
locationWatchTimer = setInterval(() => {
|
||||
refreshCurrentLocation(false)
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
// 当前使用 gcj02 定位坐标参与圈地判断,默认要求地块 boundaryGeojson 采用相同坐标系。
|
||||
// 若圈地数据来源坐标系不是 gcj02,需要在后端或数据入库环节统一,而不是在这里做隐式转换。
|
||||
async function checkLocationInLand() {
|
||||
const plotId = getSelectedPlotId()
|
||||
if (!plotId) {
|
||||
toast.show('请选择批次')
|
||||
return false
|
||||
}
|
||||
|
||||
const currentLocation = await refreshCurrentLocation(false)
|
||||
if (!currentLocation) {
|
||||
toast.show('定位获取失败,请开启定位权限后重试')
|
||||
return false
|
||||
}
|
||||
|
||||
let geoJson = currentPlotGeoJson.value
|
||||
if (!geoJson) {
|
||||
try {
|
||||
const plot = await getPlot(plotId)
|
||||
geoJson = parseBoundaryGeoJson(plot?.boundaryGeojson)
|
||||
if (geoJson) {
|
||||
applyPlotGeoJsonToMap(geoJson)
|
||||
}
|
||||
} catch {
|
||||
toast.show('地块范围获取失败,请稍后重试')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (!geoJson) {
|
||||
toast.show('该地块范围数据格式异常,暂无法提交')
|
||||
return false
|
||||
}
|
||||
|
||||
const inRange = isPointInGeoJson(currentLocation, geoJson)
|
||||
if (!inRange) {
|
||||
toast.show('当前位置不在该地块范围内,请移动到地块范围内后再提交')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** 批次选择回调 */
|
||||
function onBatchConfirm({ value }: { value: unknown }) {
|
||||
formData.value.batchId = normalizePickerValue(value)
|
||||
}
|
||||
|
||||
watch(() => formData.value.batchId, async (batchId) => {
|
||||
if (!batchId || currentId.value) {
|
||||
clearPlotMap()
|
||||
return
|
||||
}
|
||||
await loadSelectedPlotMap()
|
||||
if (!mapReady.value) {
|
||||
await refreshCurrentLocation(false)
|
||||
}
|
||||
})
|
||||
|
||||
/** 操作类型选择回调 */
|
||||
function onTypeConfirm({ value }: { value: unknown }) {
|
||||
formData.value.operationType = normalizePickerValue(value)
|
||||
}
|
||||
|
||||
/** 自定义上传方法 */
|
||||
const customUpload: UploadMethod = (file, formData, options) => {
|
||||
const uploadTask = uni.uploadFile({
|
||||
url: `${getEnvBaseUrl()}/infra/file/upload`,
|
||||
header: options.header,
|
||||
name: options.name,
|
||||
fileType: options.fileType,
|
||||
formData,
|
||||
filePath: file.url,
|
||||
success(res) {
|
||||
if (res.statusCode === options.statusCode) {
|
||||
options.onSuccess(res, file, formData)
|
||||
} else {
|
||||
options.onError({ ...res, errMsg: res.errMsg || '' }, file, formData)
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
options.onError(err, file, formData)
|
||||
},
|
||||
})
|
||||
uploadTask.onProgressUpdate((res) => {
|
||||
options.onProgress(res, file)
|
||||
})
|
||||
}
|
||||
|
||||
/** 上传变化回调 */
|
||||
function onUploadChange({ fileList: currentFileList }: { fileList: UploadFile[] }) {
|
||||
fileList.value = currentFileList || []
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-agri/operation/index')
|
||||
}
|
||||
|
||||
/** 加载批次列表 */
|
||||
async function loadBatchList() {
|
||||
try {
|
||||
const result = await getBatchSimpleList()
|
||||
batchList.value = Array.isArray(result) ? result : []
|
||||
} catch {
|
||||
batchList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!currentId.value) return
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
const data = await getOperation(currentId.value)
|
||||
const defaultFormData = createDefaultFormData()
|
||||
const normalizedBatchId = normalizePickerValue(data?.batchId)
|
||||
const normalizedOperationType = normalizePickerValue(data?.operationType)
|
||||
|
||||
formData.value = {
|
||||
...defaultFormData,
|
||||
...data,
|
||||
id: data?.id ?? currentId.value,
|
||||
batchId: batchColumns.value.some(item => item.value === normalizedBatchId) ? normalizedBatchId : undefined,
|
||||
operationType: typeColumns.value.some(item => item.value === normalizedOperationType) ? normalizedOperationType : undefined,
|
||||
operatorName: data?.operatorName || '',
|
||||
workContent: data?.workContent || '',
|
||||
remark: data?.remark || '',
|
||||
}
|
||||
|
||||
operationDateTimestamp.value = normalizeDateTimestamp(data?.operationDate)
|
||||
fileList.value = buildUploadFileList(data?.imageUrls)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建提交数据 */
|
||||
function buildSubmitData() {
|
||||
const submitData: Record<string, any> = {
|
||||
id: formData.value.id,
|
||||
batchId: formData.value.batchId,
|
||||
operationType: formData.value.operationType,
|
||||
operatorName: formData.value.operatorName || '',
|
||||
workContent: formData.value.workContent || '',
|
||||
remark: formData.value.remark || '',
|
||||
longitude: locationPoint.value?.longitude,
|
||||
latitude: locationPoint.value?.latitude,
|
||||
}
|
||||
if (operationDateTimestamp.value) {
|
||||
const date = new Date(operationDateTimestamp.value)
|
||||
if (!Number.isNaN(date.getTime())) {
|
||||
submitData.operationDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
submitData.imageUrls = fileList.value
|
||||
.filter(file => file.status === 'success')
|
||||
.map((file) => {
|
||||
if (file.response) {
|
||||
try {
|
||||
const res = typeof file.response === 'string' ? JSON.parse(file.response) : file.response
|
||||
return res?.data || file.url
|
||||
} catch {
|
||||
return file.url
|
||||
}
|
||||
}
|
||||
return file.url
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(',')
|
||||
return submitData
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value!.validate()
|
||||
if (!valid) return
|
||||
|
||||
if (!currentId.value) {
|
||||
const checked = await checkLocationInLand()
|
||||
if (!checked) return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
const submitData = buildSubmitData()
|
||||
if (currentId.value) {
|
||||
await updateOperation(submitData)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createOperation(submitData)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
formData.value = createDefaultFormData()
|
||||
fileList.value = []
|
||||
operationDateTimestamp.value = Date.now()
|
||||
await loadBatchList()
|
||||
await getDetail()
|
||||
if (!currentId.value) {
|
||||
await refreshCurrentLocation(true)
|
||||
await loadSelectedPlotMap()
|
||||
startLocationWatch()
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (!currentId.value) {
|
||||
refreshCurrentLocation(false)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopLocationWatch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.map-section {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.map-status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.map-status-text {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.map-status-text.is-success {
|
||||
color: #18a058;
|
||||
}
|
||||
|
||||
.map-status-text.is-warning {
|
||||
color: #d46b08;
|
||||
}
|
||||
|
||||
.map-status-text.is-neutral {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.plot-map {
|
||||
width: 100%;
|
||||
height: 420rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
468
src/pages-agri/operation/index.vue
Normal file
468
src/pages-agri/operation/index.vue
Normal file
@@ -0,0 +1,468 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="农事操作记录"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<view class="px-24rpx pt-24rpx">
|
||||
<view class="flex items-center">
|
||||
<view class="flex-1" @click="searchVisible = true">
|
||||
<wd-search :placeholder="searchPlaceholder" hide-cancel disabled />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作类型快速筛选 -->
|
||||
<view class="flex items-center overflow-hidden rounded-12rpx bg-white mx-24rpx mb-16rpx shadow-sm">
|
||||
<view
|
||||
v-for="tab in typeTabs"
|
||||
:key="tab.value"
|
||||
class="flex-1 py-20rpx text-center text-26rpx relative"
|
||||
:class="activeType === tab.value ? 'text-[#018d71] font-semibold' : 'text-[#666]'"
|
||||
@click="handleQuickFilter(tab.value)"
|
||||
>
|
||||
{{ tab.label }}
|
||||
<view
|
||||
v-if="activeType === tab.value"
|
||||
class="absolute bottom-0 left-1/4 w-1/2 h-4rpx rounded-2rpx bg-[#018d71]"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 农事记录列表 -->
|
||||
<view class="px-24rpx pb-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-20rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleCardClick(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<!-- 头部:批次号 + 类型标签 -->
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-30rpx text-[#333] font-semibold">
|
||||
{{ item.batchNo || '-' }}
|
||||
</view>
|
||||
<view class="rounded-8rpx px-16rpx py-4rpx text-24rpx bg-[#e8f7f4] text-[#018d71]">
|
||||
{{ getTypeLabel(item.operationType) }}
|
||||
</view>
|
||||
</view>
|
||||
<!-- 作物 -->
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">作物</text>
|
||||
<text>{{ item.cropName || '-' }}</text>
|
||||
</view>
|
||||
<!-- 操作日期 -->
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">操作日期</text>
|
||||
<text>{{ formatDate(item.operationDate) }}</text>
|
||||
</view>
|
||||
<!-- 操作人 -->
|
||||
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999]">操作人</text>
|
||||
<text>{{ item.operatorName || '-' }}</text>
|
||||
</view>
|
||||
<!-- 工作内容 -->
|
||||
<view class="mb-12rpx flex items-start justify-between text-26rpx text-[#666]">
|
||||
<text class="text-[#999] shrink-0">工作内容</text>
|
||||
<text class="text-[#333] text-right line-clamp-2 ml-16rpx" style="max-width: 500rpx;">{{ item.workContent || '-' }}</text>
|
||||
</view>
|
||||
<!-- 图片预览 -->
|
||||
<view v-if="item.imageUrls" class="flex flex-wrap mt-12rpx gap-8rpx">
|
||||
<image
|
||||
v-for="(url, idx) in getImageList(item.imageUrls)"
|
||||
:key="idx"
|
||||
:src="url"
|
||||
mode="aspectFill"
|
||||
class="image-thumb"
|
||||
@click.stop="previewImage(item.imageUrls, idx)"
|
||||
/>
|
||||
</view>
|
||||
<!-- 备注 -->
|
||||
<view v-if="item.remark" class="mt-12rpx text-24rpx text-[#909399]">
|
||||
备注:{{ item.remark }}
|
||||
</view>
|
||||
</view>
|
||||
<!-- 操作按钮 -->
|
||||
<view class="flex flex-wrap gap-12rpx px-24rpx pb-20rpx" @click.stop>
|
||||
<wd-button size="small" type="primary" plain @click="handleDetail(item)">
|
||||
详情
|
||||
</wd-button>
|
||||
<wd-button size="small" type="info" plain @click="handleEdit(item)">
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button size="small" type="error" plain @click="handleDelete(item)">
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无农事记录" />
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="searchVisible" position="top" @close="searchVisible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">批次</view>
|
||||
<wd-picker
|
||||
v-model="searchForm.batchId"
|
||||
:columns="batchColumns"
|
||||
label=""
|
||||
placeholder="请选择批次"
|
||||
@confirm="onBatchConfirm"
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">操作类型</view>
|
||||
<wd-picker
|
||||
v-model="searchForm.operationType"
|
||||
:columns="typeColumns"
|
||||
label=""
|
||||
placeholder="请选择类型"
|
||||
@confirm="onTypeConfirm"
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">重置</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">搜索</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<wd-popup v-model="detailVisible" position="bottom" custom-style="height: 70%;">
|
||||
<view class="p-24rpx">
|
||||
<view class="text-32rpx text-[#333] font-semibold mb-24rpx">记录详情</view>
|
||||
<view v-if="currentItem" class="text-26rpx text-[#666]">
|
||||
<view class="mb-16rpx flex"><text class="text-[#999] w-160rpx">批次号</text><text>{{ currentItem.batchNo || '-' }}</text></view>
|
||||
<view class="mb-16rpx flex"><text class="text-[#999] w-160rpx">作物</text><text>{{ currentItem.cropName || '-' }}</text></view>
|
||||
<view class="mb-16rpx flex"><text class="text-[#999] w-160rpx">操作类型</text><text>{{ getTypeLabel(currentItem.operationType) }}</text></view>
|
||||
<view class="mb-16rpx flex"><text class="text-[#999] w-160rpx">操作日期</text><text>{{ formatDate(currentItem.operationDate) }}</text></view>
|
||||
<view class="mb-16rpx flex"><text class="text-[#999] w-160rpx">操作人</text><text>{{ currentItem.operatorName || '-' }}</text></view>
|
||||
<view class="mb-16rpx flex"><text class="text-[#999] w-160rpx">工作内容</text><text class="flex-1 break-all">{{ currentItem.workContent || '-' }}</text></view>
|
||||
<view v-if="currentItem.imageUrls" class="mb-16rpx">
|
||||
<text class="text-[#999]">图片</text>
|
||||
<view class="flex flex-wrap mt-8rpx gap-8rpx">
|
||||
<image
|
||||
v-for="(url, idx) in getImageList(currentItem.imageUrls)"
|
||||
:key="idx"
|
||||
:src="url"
|
||||
mode="aspectFill"
|
||||
class="image-thumb-lg"
|
||||
@click="previewImage(currentItem.imageUrls, idx)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="currentItem.remark" class="mb-16rpx flex"><text class="text-[#999] w-160rpx">备注</text><text class="flex-1">{{ currentItem.remark }}</text></view>
|
||||
</view>
|
||||
<wd-button type="primary" block class="mt-32rpx" @click="detailVisible = false">关闭</wd-button>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { AgriOperationVO } from '@/api/agri/operation'
|
||||
import type { AgriBatchVO } from '@/api/agri/batch'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { OPERATION_TYPE_MAP } from '@/api/agri/operation'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getNavbarHeight, navigateBackPlus } from '@/utils'
|
||||
import {
|
||||
deleteOperation,
|
||||
getOperationPage,
|
||||
} from '@/api/agri/operation'
|
||||
import { getBatchSimpleList } from '@/api/agri/batch'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const total = ref(0)
|
||||
const list = ref<AgriOperationVO[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const batchList = ref<AgriBatchVO[]>([])
|
||||
const queryParams = ref<Record<string, any>>({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
// 搜索相关
|
||||
const searchVisible = ref(false)
|
||||
const searchForm = reactive({
|
||||
batchId: undefined as number | undefined,
|
||||
batchIdLabel: '',
|
||||
operationType: undefined as number | undefined,
|
||||
operationTypeLabel: '',
|
||||
})
|
||||
|
||||
// 详情弹窗
|
||||
const detailVisible = ref(false)
|
||||
const currentItem = ref<AgriOperationVO | null>(null)
|
||||
|
||||
// 操作类型快速筛选
|
||||
const activeType = ref<number | undefined>(undefined)
|
||||
const typeTabs = computed(() => [
|
||||
{ label: '全部', value: undefined },
|
||||
...Object.entries(OPERATION_TYPE_MAP).map(([value, label]) => ({
|
||||
label,
|
||||
value: Number(value),
|
||||
})),
|
||||
])
|
||||
|
||||
/** 批次下拉列 */
|
||||
const batchColumns = computed(() =>
|
||||
batchList.value.map(b => ({ value: b.id, label: `${b.batchNo} - ${b.cropName}` })),
|
||||
)
|
||||
|
||||
/** 操作类型下拉列 */
|
||||
const typeColumns = computed(() =>
|
||||
Object.entries(OPERATION_TYPE_MAP).map(([value, label]) => ({ value: Number(value), label })),
|
||||
)
|
||||
|
||||
/** 搜索条件 placeholder */
|
||||
const searchPlaceholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (searchForm.batchIdLabel) conditions.push(`批次:${searchForm.batchIdLabel}`)
|
||||
if (searchForm.operationTypeLabel) conditions.push(`类型:${searchForm.operationTypeLabel}`)
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索农事记录'
|
||||
})
|
||||
|
||||
/** 格式化日期 */
|
||||
function formatDate(date?: string | number | Date) {
|
||||
if (!date) return '-'
|
||||
if (typeof date === 'number') {
|
||||
const d = new Date(date)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
if (typeof date === 'object' && date instanceof Date) {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
return String(date).substring(0, 10)
|
||||
}
|
||||
|
||||
/** 获取操作类型标签 */
|
||||
function getTypeLabel(type?: number) {
|
||||
if (type === undefined || type === null) return '-'
|
||||
return OPERATION_TYPE_MAP[type] || '-'
|
||||
}
|
||||
|
||||
/** 获取图片列表 */
|
||||
function getImageList(imageUrls?: string) {
|
||||
if (!imageUrls) return []
|
||||
return imageUrls.split(',').filter(u => u.trim())
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 加载批次列表 */
|
||||
async function loadBatchList() {
|
||||
try {
|
||||
batchList.value = await getBatchSimpleList()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const params: Record<string, any> = { ...queryParams.value }
|
||||
if (searchForm.batchId) params.batchId = searchForm.batchId
|
||||
if (searchForm.operationType !== undefined && searchForm.operationType !== null) {
|
||||
params.operationType = searchForm.operationType
|
||||
}
|
||||
const data = await getOperationPage(params)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 操作类型快速筛选 */
|
||||
function handleQuickFilter(type?: number) {
|
||||
activeType.value = type
|
||||
if (type !== undefined) {
|
||||
queryParams.value.operationType = type
|
||||
} else {
|
||||
delete queryParams.value.operationType
|
||||
}
|
||||
queryParams.value.pageNo = 1
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
searchVisible.value = false
|
||||
queryParams.value = {
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
activeType.value = undefined
|
||||
searchForm.batchId = undefined
|
||||
searchForm.batchIdLabel = ''
|
||||
searchForm.operationType = undefined
|
||||
searchForm.operationTypeLabel = ''
|
||||
searchVisible.value = false
|
||||
queryParams.value = { pageNo: 1, pageSize: 10 }
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 批次选择回调 */
|
||||
function onBatchConfirm({ value }: any) {
|
||||
searchForm.batchId = value?.[0] || undefined
|
||||
searchForm.batchIdLabel = value?.[0]
|
||||
? (batchList.value.find(b => b.id === value[0])?.batchNo || '')
|
||||
: ''
|
||||
}
|
||||
|
||||
/** 操作类型选择回调 */
|
||||
function onTypeConfirm({ value }: any) {
|
||||
searchForm.operationType = value?.[0] ?? undefined
|
||||
searchForm.operationTypeLabel = value?.[0] !== undefined
|
||||
? OPERATION_TYPE_MAP[value[0]] || ''
|
||||
: ''
|
||||
}
|
||||
|
||||
/** 卡片点击 — 打开详情 */
|
||||
function handleCardClick(item: AgriOperationVO) {
|
||||
handleDetail(item)
|
||||
}
|
||||
|
||||
/** 详情 */
|
||||
function handleDetail(item: AgriOperationVO) {
|
||||
currentItem.value = item
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-agri/operation/form/index',
|
||||
fail: (err) => {
|
||||
console.error('[DEBUG] navigateTo handleAdd failed:', err)
|
||||
uni.showToast({ title: '页面跳转失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit(item: AgriOperationVO) {
|
||||
if (!item?.id) {
|
||||
toast.show('当前记录缺少 ID')
|
||||
return
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages-agri/operation/form/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete(item: AgriOperationVO) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定删除该农事记录?',
|
||||
confirmColor: '#f5222d',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
uni.showLoading({ title: '删除中...' })
|
||||
await deleteOperation(item.id)
|
||||
toast.success('删除成功')
|
||||
list.value = []
|
||||
queryParams.value = { pageNo: 1, pageSize: 10 }
|
||||
getList()
|
||||
} catch {
|
||||
toast.show('删除失败')
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') return
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 图片预览 */
|
||||
function previewImage(imageUrls: string, index: number) {
|
||||
const urls = getImageList(imageUrls)
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current: index,
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadBatchList()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-thumb {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.image-thumb-lg {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -100,6 +100,14 @@ const menuGroupsData: MenuGroup[] = [
|
||||
iconColor: '#52c41a',
|
||||
permission: 'agri:biz:harvest',
|
||||
},
|
||||
{
|
||||
key: 'agriOperation',
|
||||
name: '农事记录',
|
||||
icon: 'note',
|
||||
url: '/pages-agri/operation/index',
|
||||
iconColor: '#fa8c16',
|
||||
permission: 'agri:operation:query',
|
||||
},
|
||||
{
|
||||
key: 'agriTraceSnapshot',
|
||||
name: '溯源快照',
|
||||
|
||||
156
src/utils/geo.js
Normal file
156
src/utils/geo.js
Normal file
@@ -0,0 +1,156 @@
|
||||
export function isPointInGeoJson(point, geoJson) {
|
||||
if (!point || !isFiniteNumber(point.longitude) || !isFiniteNumber(point.latitude) || !geoJson) {
|
||||
return false
|
||||
}
|
||||
|
||||
const geometries = collectGeometries(geoJson)
|
||||
if (!geometries.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return geometries.some((geometry) => {
|
||||
if (!geometry || !geometry.type || !geometry.coordinates) {
|
||||
return false
|
||||
}
|
||||
if (geometry.type === 'Polygon') {
|
||||
return isPointInPolygonCoordinates(point, geometry.coordinates)
|
||||
}
|
||||
if (geometry.type === 'MultiPolygon') {
|
||||
return geometry.coordinates.some(polygon => isPointInPolygonCoordinates(point, polygon))
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
export function getGeoJsonPolygonPaths(geoJson) {
|
||||
const geometries = collectGeometries(geoJson)
|
||||
if (!geometries.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return geometries.flatMap((geometry) => {
|
||||
if (!geometry || !geometry.type || !geometry.coordinates) {
|
||||
return []
|
||||
}
|
||||
if (geometry.type === 'Polygon') {
|
||||
return buildPolygonPaths(geometry.coordinates)
|
||||
}
|
||||
if (geometry.type === 'MultiPolygon') {
|
||||
return geometry.coordinates.flatMap(polygon => buildPolygonPaths(polygon))
|
||||
}
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
function collectGeometries(geoJson) {
|
||||
if (!geoJson || typeof geoJson !== 'object') {
|
||||
return []
|
||||
}
|
||||
|
||||
if (geoJson.type === 'Feature') {
|
||||
return geoJson.geometry ? [geoJson.geometry] : []
|
||||
}
|
||||
|
||||
if (geoJson.type === 'FeatureCollection') {
|
||||
return (geoJson.features || []).flatMap(feature => collectGeometries(feature))
|
||||
}
|
||||
|
||||
if (geoJson.type === 'Polygon' || geoJson.type === 'MultiPolygon') {
|
||||
return [geoJson]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function isPointInPolygonCoordinates(point, polygonCoordinates) {
|
||||
if (!Array.isArray(polygonCoordinates) || polygonCoordinates.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const [outerRing, ...holes] = polygonCoordinates
|
||||
if (!isPointInRing(point, outerRing)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !holes.some(hole => isPointInRing(point, hole))
|
||||
}
|
||||
|
||||
function buildPolygonPaths(polygonCoordinates) {
|
||||
if (!Array.isArray(polygonCoordinates) || polygonCoordinates.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const [outerRing] = polygonCoordinates
|
||||
const points = normalizeRingPoints(outerRing)
|
||||
return points.length >= 3 ? [points] : []
|
||||
}
|
||||
|
||||
function normalizeRingPoints(ring) {
|
||||
if (!Array.isArray(ring)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return ring
|
||||
.filter(isValidCoordinate)
|
||||
.map(coordinate => ({
|
||||
longitude: Number(coordinate[0]),
|
||||
latitude: Number(coordinate[1]),
|
||||
}))
|
||||
}
|
||||
|
||||
function isPointInRing(point, ring) {
|
||||
if (!Array.isArray(ring) || ring.length < 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
const x = Number(point.longitude)
|
||||
const y = Number(point.latitude)
|
||||
let inside = false
|
||||
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
|
||||
const current = ring[i]
|
||||
const previous = ring[j]
|
||||
if (!isValidCoordinate(current) || !isValidCoordinate(previous)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const xi = Number(current[0])
|
||||
const yi = Number(current[1])
|
||||
const xj = Number(previous[0])
|
||||
const yj = Number(previous[1])
|
||||
|
||||
if (isPointOnSegment(x, y, xi, yi, xj, yj)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const intersects = ((yi > y) !== (yj > y))
|
||||
&& (x <= ((xj - xi) * (y - yi)) / (yj - yi) + xi)
|
||||
|
||||
if (intersects) {
|
||||
inside = !inside
|
||||
}
|
||||
}
|
||||
|
||||
return inside
|
||||
}
|
||||
|
||||
function isPointOnSegment(px, py, x1, y1, x2, y2) {
|
||||
const cross = (px - x1) * (y2 - y1) - (py - y1) * (x2 - x1)
|
||||
if (Math.abs(cross) > 1e-10) {
|
||||
return false
|
||||
}
|
||||
|
||||
const dot = (px - x1) * (px - x2) + (py - y1) * (py - y2)
|
||||
return dot <= 1e-10
|
||||
}
|
||||
|
||||
function isValidCoordinate(coordinate) {
|
||||
return Array.isArray(coordinate)
|
||||
&& coordinate.length >= 2
|
||||
&& isFiniteNumber(coordinate[0])
|
||||
&& isFiniteNumber(coordinate[1])
|
||||
}
|
||||
|
||||
function isFiniteNumber(value) {
|
||||
return Number.isFinite(Number(value))
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export function getEnvBaseUrl() {
|
||||
|
||||
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
|
||||
// TODO @芋艿:这个后续也要调整。
|
||||
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'http://localhost:48080/admin-api'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'http://192.168.1.107:48080/admin-api'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'http://119.96.62.56:7004/admin-api'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'http://localhost:48080/admin-api'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user