Compare commits
2 Commits
dev-common
...
dev-stny
| 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_APP_PUBLIC_BASE=/
|
||||||
|
|
||||||
# 后台请求地址
|
# 后台请求地址
|
||||||
VITE_SERVER_BASEURL = 'http://localhost:48080/admin-api'
|
VITE_SERVER_BASEURL = 'http://192.168.1.107:48080/admin-api'
|
||||||
VITE_UPLOAD_BASEURL = 'http://localhost:48080/upload'
|
VITE_UPLOAD_BASEURL = 'http://192.168.1.107:48080/upload'
|
||||||
# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
|
# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
|
||||||
|
|
||||||
# 注意,如果是微信小程序,还有一套请求地址的配置,根据 develop、trial、release 分别设置上传地址,见 `src/utils/index.ts`。
|
# 注意,如果是微信小程序,还有一套请求地址的配置,根据 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_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',
|
iconColor: '#52c41a',
|
||||||
permission: 'agri:biz:harvest',
|
permission: 'agri:biz:harvest',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'agriOperation',
|
||||||
|
name: '农事记录',
|
||||||
|
icon: 'note',
|
||||||
|
url: '/pages-agri/operation/index',
|
||||||
|
iconColor: '#fa8c16',
|
||||||
|
permission: 'agri:operation:query',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'agriTraceSnapshot',
|
key: 'agriTraceSnapshot',
|
||||||
name: '溯源快照',
|
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 分别设置上传地址,参考代码如下。
|
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
|
||||||
// TODO @芋艿:这个后续也要调整。
|
// 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_TRIAL = 'http://119.96.62.56:7004/admin-api'
|
||||||
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'http://localhost:48080/admin-api'
|
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'http://localhost:48080/admin-api'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user