index.vue 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295
  1. <template>
  2. <div class="create-farm">
  3. <custom-header
  4. :name="paramsType === 'client' ? '新增用户' : paramsType === 'edit' ? '编辑农场' : '创建农场'"
  5. :isGoBack="true"
  6. @goback="backgToHome"
  7. ></custom-header>
  8. <!-- 地图 -->
  9. <div class="map-container" ref="mapContainer"></div>
  10. <div class="farm-content">
  11. <div class="top-mask"></div>
  12. <div class="farm-filter">
  13. <el-select
  14. v-model="locationVal"
  15. filterable
  16. remote
  17. reserve-keyword
  18. placeholder="搜索位置"
  19. :remote-method="remoteMethod"
  20. :loading="loading"
  21. @change="handleSearchRes"
  22. popper-class="location-search-popper"
  23. >
  24. <el-option
  25. v-for="(item, index) in locationOptions.list"
  26. :key="index"
  27. :label="item.title"
  28. :value="{ value: item.point, item }"
  29. >
  30. <span>{{ item.title }}</span>
  31. <span class="sub-title">{{ item.province }}{{ item.city }}{{ item.district }}</span>
  32. </el-option>
  33. </el-select>
  34. </div>
  35. <!-- 创建 -->
  36. <div class="create-wrap">
  37. <div class="create-box">
  38. <div class="box-content">
  39. <div class="create-title">
  40. <img class="title-icon" src="@/assets/img/home/create-icon.png" alt="" />
  41. {{ paramsType === "client" ? "新增用户" : paramsType === "edit" ? "编辑农场" : "创建农场" }}
  42. </div>
  43. <div class="create-content">
  44. <div class="create-from">
  45. <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" class="demo-ruleForm">
  46. <el-form-item label="农场位置" prop="address">
  47. <div class="position-wrap">
  48. <el-input
  49. placeholder="农场位置"
  50. readonly
  51. v-model="ruleForm.address"
  52. autocomplete="off"
  53. />
  54. <div class="draw-btn" @click="toSubPage">
  55. {{ hasDefaultPolygon ? "点击勾选地块" : "新增地块" }}
  56. </div>
  57. </div>
  58. </el-form-item>
  59. <el-form-item label="种植作物" prop="speciesItem">
  60. <div class="select-wrap">
  61. <el-select
  62. @change="changeSpecie"
  63. class="select-item"
  64. v-model="ruleForm.speciesItem"
  65. placeholder="品类"
  66. >
  67. <el-option
  68. v-for="(item, index) in specieList"
  69. :key="index"
  70. :label="item.name"
  71. :value="{ value: item.id, ...item }"
  72. />
  73. </el-select>
  74. <el-select
  75. v-model="ruleForm.typeId"
  76. placeholder="品种"
  77. class="period-select select-item"
  78. >
  79. <el-option
  80. v-for="(item, index) in fruitsList"
  81. :key="index"
  82. :label="item.name"
  83. :value="item.id"
  84. />
  85. </el-select>
  86. </div>
  87. </el-form-item>
  88. <el-form-item v-if="paramsType !== 'farmer'" label="联系人" prop="fzr">
  89. <div class="area-box">
  90. <el-input
  91. placeholder="请输入联系人姓名"
  92. v-model="ruleForm.fzr"
  93. autocomplete="off"
  94. style="width: fit-content"
  95. />
  96. </div>
  97. </el-form-item>
  98. <el-form-item v-if="paramsType !== 'farmer'" label="联系电话" prop="tel">
  99. <div class="area-box">
  100. <el-input
  101. placeholder="请输入联系人电话"
  102. v-model="ruleForm.tel"
  103. autocomplete="off"
  104. style="width: fit-content"
  105. />
  106. </div>
  107. </el-form-item>
  108. <el-form-item label="农场面积" prop="mu">
  109. <div class="area-box">
  110. <el-input
  111. :placeholder="
  112. isFromEditMap ? '勾选地块获得农场面积' : '请输入农场的真实面积'
  113. "
  114. v-model="ruleForm.mu"
  115. :readonly="isFromEditMap"
  116. type="text"
  117. autocomplete="off"
  118. style="width: fit-content"
  119. @input="handleMianjiInput"
  120. @keypress="handleMianjiKeypress"
  121. />
  122. <div class="unit">亩</div>
  123. </div>
  124. </el-form-item>
  125. <el-form-item
  126. v-if="paramsType !== 'farmer'"
  127. label="客户类型"
  128. prop="userType"
  129. class="select-wrap client-wrap"
  130. >
  131. <el-select class="select-item" v-model="ruleForm.userType" placeholder="请选择">
  132. <el-option label="普通用户" :value="1" />
  133. <el-option label="托管用户" :value="2" />
  134. <el-option label="优质客户" :value="3" />
  135. </el-select>
  136. </el-form-item>
  137. <el-form-item label="农场名称" prop="name">
  138. <el-input
  139. placeholder="请输入您的农场名称"
  140. v-model="ruleForm.name"
  141. autocomplete="off"
  142. @input="handleFarmNameInput"
  143. />
  144. </el-form-item>
  145. <!-- <Checkbox
  146. v-if="paramsType !== 'client' && curRole == 0"
  147. class="checkbox"
  148. icon-size="18px"
  149. shape="square"
  150. v-model="ruleForm.defaultFarm"
  151. >是否勾选为默认农场</Checkbox
  152. > -->
  153. </el-form>
  154. </div>
  155. <div class="create-btn">
  156. <div class="btn-item sencond-btn" @click="resetForm(ruleFormRef)">取消</div>
  157. <div class="btn-item primary-btn" @click="submitForm(ruleFormRef)">
  158. {{
  159. paramsType === "client"
  160. ? "添加"
  161. : paramsType === "edit"
  162. ? "确认修改"
  163. : "立即创建"
  164. }}
  165. </div>
  166. </div>
  167. </div>
  168. </div>
  169. </div>
  170. </div>
  171. </div>
  172. </div>
  173. <!-- 农情采集成功弹窗 -->
  174. <tip-popup
  175. v-model:show="showSuccessPopup"
  176. type="success"
  177. text="农情采集成功"
  178. @confirm="handlePopupConfirm"
  179. @handleClickOverlay="handlePopupConfirm"
  180. />
  181. </template>
  182. <script setup>
  183. import customHeader from "@/components/customHeader.vue";
  184. import IndexMap from "./map/index.js";
  185. import { useRoute, useRouter } from "vue-router";
  186. import { mapLocation } from "./map/index.js";
  187. import { onMounted, ref, reactive, watch, onActivated, nextTick } from "vue";
  188. import { useStore } from "vuex";
  189. import { convertPointToArray } from "@/utils/index";
  190. import { ElMessage } from "element-plus";
  191. import { Checkbox } from "vant";
  192. import tipPopup from "@/components/popup/tipPopup.vue";
  193. import wx from "weixin-js-sdk";
  194. import { transformFromGCJToWGS, transformFromWGSToGCJ } from "@/utils/WSCoordinate.js";
  195. const route = useRoute();
  196. const router = useRouter();
  197. const store = useStore();
  198. const indexMap = new IndexMap();
  199. const mapContainer = ref(null);
  200. // 农情采集成功弹窗
  201. const showSuccessPopup = ref(false);
  202. const handlePopupConfirm = () => {
  203. showSuccessPopup.value = false;
  204. router.replace(`/home`);
  205. };
  206. // 标记是否从编辑地图页面确认返回
  207. const isFromEditMap = ref(false);
  208. // 标记是否已创建默认地块
  209. const hasDefaultPolygon = ref(false);
  210. // 标记用户是否手动修改过农场名称
  211. const isFarmNameManuallyModified = ref(false);
  212. /**
  213. * 根据中心点生成指定边长的正方形地块WKT
  214. * @param {Array} center - 中心点坐标 [lng, lat]
  215. * @param {Number} sideLength - 正方形边长(米)
  216. * @returns {Object} 包含WKT字符串和面积(亩)的对象
  217. */
  218. function generateSquarePolygonBySideLength(center, sideLength) {
  219. // 确保输入是数字类型
  220. const lng = parseFloat(center[0]);
  221. const lat = parseFloat(center[1]);
  222. // 半边长(米)
  223. const halfSide = sideLength / 2;
  224. // 纬度方向:1度约等于111000米
  225. const latDelta = halfSide / 111000;
  226. // 经度方向:需要根据纬度调整,1度约等于111000 * cos(lat)米
  227. const lngDelta = halfSide / (111000 * Math.cos((lat * Math.PI) / 180));
  228. // 计算四个顶点(逆时针顺序)
  229. const topLeft = [lng - lngDelta, lat + latDelta];
  230. const topRight = [lng + lngDelta, lat + latDelta];
  231. const bottomRight = [lng + lngDelta, lat - latDelta];
  232. const bottomLeft = [lng - lngDelta, lat - latDelta];
  233. // 生成MULTIPOLYGON格式的WKT(需要闭合,所以第一个点要重复)
  234. // 明确使用join来格式化坐标点
  235. const formatPoint = (point) => `${point[0]} ${point[1]}`;
  236. const coordinates = [topLeft, topRight, bottomRight, bottomLeft, topLeft].map(formatPoint).join(", ");
  237. // 注意:MULTIPOLYGON 和括号之间需要有一个空格
  238. const wkt = `MULTIPOLYGON (((${coordinates})))`;
  239. // 计算面积(亩): 边长200米 = 40,000平方米 ≈ 60亩
  240. const areaInSquareMeters = sideLength * sideLength;
  241. const areaInMu = (areaInSquareMeters / 666.67).toFixed(2);
  242. return { wkt, area: areaInMu };
  243. }
  244. /**
  245. * 根据中心点和亩数生成正方形地块WKT
  246. * @param {Array} center - 中心点坐标 [lng, lat]
  247. * @param {Number} mu - 面积(亩)
  248. * @returns {Object} 包含WKT字符串和面积(亩)的对象
  249. */
  250. function generateSquarePolygonByMu(center, mu) {
  251. // 确保输入是数字类型
  252. const lng = parseFloat(center[0]);
  253. const lat = parseFloat(center[1]);
  254. // 1亩 ≈ 666.67平方米
  255. const areaInSquareMeters = mu * 666.67;
  256. // 正方形边长(米)
  257. const sideLength = Math.sqrt(areaInSquareMeters);
  258. // 半边长(米)
  259. const halfSide = sideLength / 2;
  260. // 纬度方向:1度约等于111000米
  261. const latDelta = halfSide / 111000;
  262. // 经度方向:需要根据纬度调整,1度约等于111000 * cos(lat)米
  263. const lngDelta = halfSide / (111000 * Math.cos((lat * Math.PI) / 180));
  264. // 计算四个顶点(逆时针顺序)
  265. const topLeft = [lng - lngDelta, lat + latDelta];
  266. const topRight = [lng + lngDelta, lat + latDelta];
  267. const bottomRight = [lng + lngDelta, lat - latDelta];
  268. const bottomLeft = [lng - lngDelta, lat - latDelta];
  269. // 生成MULTIPOLYGON格式的WKT(需要闭合,所以第一个点要重复)
  270. const formatPoint = (point) => `${point[0]} ${point[1]}`;
  271. const coordinates = [topLeft, topRight, bottomRight, bottomLeft, topLeft].map(formatPoint).join(", ");
  272. // 注意:MULTIPOLYGON 和括号之间需要有一个空格
  273. const wkt = `MULTIPOLYGON (((${coordinates})))`;
  274. return { wkt, area: parseFloat(mu).toFixed(2) };
  275. }
  276. onMounted(() => {
  277. // 清除上一次的地块数据,确保每次进入都是全新状态
  278. store.commit("home/SET_FARM_POLYGON", null);
  279. isFromEditMap.value = false; // 初始化时可以手动输入
  280. hasDefaultPolygon.value = false; // 初始化时没有默认地块
  281. centerPoint.value = store.state.home.miniUserLocationPoint;
  282. const arr = convertPointToArray(centerPoint.value);
  283. getLocationName(`${arr[1]},${arr[0]}`);
  284. indexMap.initMap(centerPoint.value, mapContainer.value);
  285. // 不再初始化时绘制默认地块,等待用户点击"新增地块"按钮
  286. // 清空地块和面积数据
  287. polygonArr.value = null;
  288. ruleForm.mu = "";
  289. ruleForm.typeId = "";
  290. // 如果是编辑模式,等待品类列表加载完成后再回填数据
  291. if (route.query.type === "edit" && store.state.home.editFarmData) {
  292. getSpecieList().then(() => {
  293. populateEditData();
  294. });
  295. } else {
  296. getSpecieList();
  297. }
  298. });
  299. const polygonArr = ref(null);
  300. const paramsType = ref(null);
  301. onActivated(() => {
  302. paramsType.value = route.query.type;
  303. // 仅在携带 isReload 标记、且不是编辑/小程序回流场景时,认为是一次全新创建,重置表单和地块,
  304. // 避免破坏原有自动生成农场名称等逻辑
  305. if (route.query.isReload && paramsType.value !== "edit" && !route.query.miniJson) {
  306. // 重置表单字段到初始值
  307. ruleFormRef.value && ruleFormRef.value.resetFields();
  308. // 重置与地块绘制相关的内部状态
  309. polygonArr.value = null;
  310. isFromEditMap.value = false;
  311. hasDefaultPolygon.value = false;
  312. // 重置农场名称手动修改标记,允许自动生成农场名称
  313. isFarmNameManuallyModified.value = false;
  314. // 清空上一次地块缓存
  315. store.commit("home/SET_FARM_POLYGON", null);
  316. }
  317. // 确保地图已初始化,使用 nextTick 等待 DOM 更新
  318. nextTick(() => {
  319. // 检查地图实例是否已初始化
  320. if (!indexMap.kmap) {
  321. // 如果地图未初始化,重新初始化
  322. if (mapContainer.value) {
  323. centerPoint.value = store.state.home.miniUserLocationPoint;
  324. const arr = convertPointToArray(centerPoint.value);
  325. indexMap.initMap(centerPoint.value, mapContainer.value);
  326. }
  327. // 等待地图初始化完成后再继续
  328. setTimeout(() => {
  329. handleMapUpdate();
  330. }, 100);
  331. return;
  332. }
  333. handleMapUpdate();
  334. });
  335. });
  336. // 处理地图更新的逻辑
  337. function handleMapUpdate() {
  338. if (route.query.isReload) {
  339. // 清除旧的地块数据
  340. store.commit("home/SET_FARM_POLYGON", null);
  341. isFromEditMap.value = false; // 从home进入,可以手动输入
  342. hasDefaultPolygon.value = false; // 重置默认地块状态
  343. centerPoint.value = store.state.home.miniUserLocationPoint;
  344. const arr = convertPointToArray(centerPoint.value);
  345. getLocationName(`${arr[1]},${arr[0]}`);
  346. indexMap.setMapPosition(arr);
  347. indexMap.clearLayer();
  348. // 不再自动生成默认地块,等待用户点击"新增地块"
  349. polygonArr.value = null;
  350. ruleForm.mu = "";
  351. ruleForm.typeId = "";
  352. ruleForm.defaultFarm = false;
  353. return; // 直接返回,不执行下面的逻辑
  354. }
  355. indexMap.clearLayer();
  356. // 绘制勾画范围(从edit_map返回的情况)
  357. const polygonData = store.state.home.polygonData;
  358. // 优先处理从编辑地图页面返回的数据
  359. if (polygonData) {
  360. // 检查地块数据是否有效(数组存在且不为空)
  361. const hasValidGeometry =
  362. polygonData.geometryArr && Array.isArray(polygonData.geometryArr) && polygonData.geometryArr.length > 0;
  363. if (hasValidGeometry) {
  364. // 用户从edit_map返回,且有有效的地块数据
  365. // 根据 isConfirmed 判断是否锁定输入框
  366. if (polygonData.isConfirmed) {
  367. // 用户点击了"确认"按钮,锁定输入框并显示精确面积
  368. isFromEditMap.value = true;
  369. ruleForm.mu = polygonData.mianji;
  370. } else {
  371. // 用户点击了"取消",不锁定输入框,面积保持原样
  372. isFromEditMap.value = false;
  373. // 面积输入框保持之前的值,不更新
  374. }
  375. // 使用 nextTick 确保地图渲染完成后再设置地块
  376. nextTick(() => {
  377. indexMap.setAreaGeometry(polygonData.geometryArr);
  378. });
  379. polygonArr.value = polygonData.geometryArr;
  380. // 有地块数据时,标记已创建默认地块
  381. hasDefaultPolygon.value = true;
  382. } else {
  383. // 用户在编辑页面删除了所有地块
  384. // 重置所有状态
  385. polygonArr.value = null;
  386. ruleForm.mu = "";
  387. isFromEditMap.value = false;
  388. hasDefaultPolygon.value = false;
  389. ruleForm.defaultFarm = false;
  390. }
  391. } else if (route.query.type === "edit" && store.state.home.editFarmData) {
  392. // 如果是编辑模式且没有从编辑地图返回的数据,回填原始编辑数据
  393. populateEditData();
  394. } else if (centerPoint.value && polygonArr.value) {
  395. // 没有新的编辑数据,保持当前地块
  396. // 同样需要检查数据有效性
  397. if (Array.isArray(polygonArr.value) && polygonArr.value.length > 0) {
  398. nextTick(() => {
  399. indexMap.setAreaGeometry(polygonArr.value);
  400. });
  401. // 保持 hasDefaultPolygon 状态
  402. }
  403. }
  404. }
  405. // 搜索
  406. const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
  407. const locationVal = ref(null);
  408. const locationOptions = reactive({
  409. list: [],
  410. });
  411. const loading = ref(false);
  412. const remoteMethod = async (keyword) => {
  413. if (keyword) {
  414. locationOptions.list = [];
  415. loading.value = true;
  416. const params = {
  417. key: MAP_KEY,
  418. keyword,
  419. location: route.query.userLocation || "113.61702297075017,23.584863449735067",
  420. };
  421. await VE_API.old_mini_map.getCtiyList({ word: keyword }).then(({ data }) => {
  422. if (data && data.length) {
  423. data.forEach((item) => {
  424. item.point = item.location.lat + "," + item.location.lng;
  425. locationOptions.list.push(item);
  426. });
  427. }
  428. });
  429. VE_API.old_mini_map.search(params).then(({ data }) => {
  430. loading.value = false;
  431. data.forEach((item) => {
  432. item.point = item.location.lat + "," + item.location.lng;
  433. locationOptions.list.push(item);
  434. });
  435. });
  436. } else {
  437. locationOptions.list = [];
  438. }
  439. };
  440. const handleSearchRes = (v) => {
  441. const parts = v.value.split(",");
  442. let { latitude, longitude } = transformFromGCJToWGS(parseFloat(parts[0]), parseFloat(parts[1]));
  443. const coordinateArray = [longitude, latitude];
  444. indexMap.setMapPosition(coordinateArray);
  445. centerPoint.value = `POINT (${coordinateArray[0]} ${coordinateArray[1]})`;
  446. ruleForm.address = v.item?.title || v.item?.address;
  447. pointAddress.value = v.item?.province + v.item?.city + v.item?.district;
  448. // 更新farmCity以便后续更新农场名称
  449. farmCity.value = v.item?.city + v.item?.district || "";
  450. // 地址修改后,如果满足条件则自动更新农场名称
  451. updateFarmNameIfNeeded();
  452. getSpecieList();
  453. };
  454. // 表单
  455. const ruleFormRef = ref(null);
  456. const ruleForm = reactive({
  457. address: "",
  458. mu: "",
  459. speciesItem: null,
  460. typeId: "",
  461. name: "",
  462. fzr: "",
  463. tel: "",
  464. userType: 1,
  465. defaultFarm: 0, // 0:否 1:是
  466. });
  467. // 自定义验证规则:验证面积必须是大于0的数字
  468. const validateMianji = (rule, value, callback) => {
  469. if (!value) {
  470. callback(new Error("请输入农场面积"));
  471. } else {
  472. const num = parseFloat(value);
  473. if (isNaN(num)) {
  474. callback(new Error("面积必须是数字"));
  475. } else if (num <= 0) {
  476. callback(new Error("面积必须大于0"));
  477. } else {
  478. callback();
  479. }
  480. }
  481. };
  482. const rules = reactive({
  483. address: [{ required: true, message: "请选择农场位置", trigger: "blur" }],
  484. userType: [{ required: true, message: "请选择客户类型", trigger: "blur" }],
  485. mu: [
  486. { required: true, message: "请输入农场面积", trigger: "blur" },
  487. { validator: validateMianji, trigger: ["blur", "change"] },
  488. ],
  489. speciesItem: [{ required: true, message: "请选择品类", trigger: "blur" }],
  490. typeId: [{ required: true, message: "请选择品种", trigger: "blur" }],
  491. name: [{ required: true, message: "请输入您的农场名称", trigger: ["blur", "change"] }],
  492. fzr: [{ required: true, message: "请输入联系人姓名", trigger: ["blur", "change"] }],
  493. tel: [
  494. { required: true, message: "请输入联系人电话", trigger: ["blur"] },
  495. {
  496. pattern: /^1[3-9]\d{9}$/,
  497. message: "请输入正确的手机号码",
  498. trigger: ["blur"],
  499. },
  500. ],
  501. defaultFarm: [{ required: true, message: "请选择是否为默认农场", trigger: "blur" }],
  502. });
  503. const curRole = localStorage.getItem("SET_USER_CUR_ROLE");
  504. const submitForm = (formEl) => {
  505. if (!formEl) return;
  506. formEl.validate((valid) => {
  507. if (valid) {
  508. const params = {
  509. ...ruleForm,
  510. wkt: centerPoint.value,
  511. speciesId: ruleForm.speciesItem?.id,
  512. containerId: ruleForm.speciesItem?.defaultContainerId,
  513. agriculturalCreate: route.query.type === "client" ? 1 : 0,
  514. // 编辑时geom不是数组,新增时是数组
  515. geom:
  516. route.query.type === "edit"
  517. ? polygonArr.value && polygonArr.value.length > 0
  518. ? polygonArr.value[0]
  519. : null
  520. : polygonArr.value,
  521. };
  522. // 如果是编辑模式,添加农场ID
  523. if (route.query.type === "edit" && route.query.farmId) {
  524. params.farmId = route.query.farmId;
  525. }
  526. if (route.query.type === "client") {
  527. // 处理 geom 参数,如果是数组需要序列化
  528. const queryParams = {
  529. ...params,
  530. ...route.query,
  531. };
  532. // 如果 geom 是数组,需要序列化为 JSON 字符串
  533. if (Array.isArray(queryParams.geom)) {
  534. queryParams.geom = JSON.stringify(queryParams.geom);
  535. }
  536. queryParams.speciesName = ruleForm.speciesItem?.name;
  537. router.push({
  538. path: "/prescription",
  539. query: queryParams,
  540. });
  541. return;
  542. }
  543. if(route.query.type === "farmer") {
  544. VE_API.farm.saveFarm({...params, expertMiniUserId: route.query.expertMiniUserId}).then((res) => {
  545. if (res.code === 0) {
  546. router.go(-1);
  547. } else {
  548. ElMessage.error(res.msg || '创建失败');
  549. }
  550. });
  551. return;
  552. }
  553. const apiCall = route.query.type === "edit" ? VE_API.farm.updateFarm(params) : VE_API.farm.saveFarm(params);
  554. apiCall.then((res) => {
  555. if (res.code === 0) {
  556. ElMessage.success(route.query.type === "edit" ? "修改成功" : "创建成功");
  557. // 重置表单和地块数据
  558. ruleFormRef.value.resetFields();
  559. store.commit("home/SET_FARM_POLYGON", null);
  560. store.commit("home/SET_EDIT_FARM_DATA", null); // 清除编辑数据
  561. polygonArr.value = null;
  562. isFromEditMap.value = false;
  563. if (route.query.type !== "edit" && curRole == 0) {
  564. localStorage.setItem("selectedFarmId", res.data.id);
  565. localStorage.setItem("selectedFarmName", res.data.name);
  566. }
  567. // 根据来源页面决定跳转目标
  568. const fromPage = route.query.from;
  569. if (fromPage && fromPage !== "details") {
  570. // 如果是从monitor页面来的
  571. router.replace(`/${fromPage}`);
  572. } else if (fromPage === "details") {
  573. router.go(-1);
  574. } else {
  575. if (route.query.miniJson) {
  576. const json = JSON.parse(route.query.miniJson);
  577. //上传图片
  578. VE_API.ali
  579. .uploadImg({
  580. farmId: res.data.id,
  581. images: json.images,
  582. uploadDate: formatDate(new Date()),
  583. })
  584. .then(({ code, msg }) => {
  585. if (code === 0) {
  586. showSuccessPopup.value = true;
  587. } else {
  588. ElMessage.error(msg);
  589. }
  590. });
  591. } else {
  592. router.replace(`/home`);
  593. }
  594. }
  595. } else {
  596. ElMessage.error(res.msg);
  597. }
  598. });
  599. }
  600. });
  601. };
  602. const resetForm = (formEl) => {
  603. if (!formEl) return;
  604. formEl.resetFields();
  605. // 清除地块数据
  606. store.commit("home/SET_FARM_POLYGON", null);
  607. polygonArr.value = null;
  608. isFromEditMap.value = false;
  609. hasDefaultPolygon.value = false; // 重置默认地块状态
  610. // 根据来源页面决定返回目标
  611. const fromPage = route.query.from;
  612. if (fromPage && fromPage !== "details") {
  613. router.replace(`/${fromPage}`);
  614. return;
  615. }
  616. router.go(-1);
  617. };
  618. const centerPoint = ref(null);
  619. function toSubPage() {
  620. // 如果还没有默认地块,先创建默认地块
  621. if (!hasDefaultPolygon.value) {
  622. if (centerPoint.value) {
  623. const arr = convertPointToArray(centerPoint.value);
  624. const squareData = generateSquarePolygonBySideLength(arr, 200); // 200米边长
  625. const geometryData = {
  626. geometryArr: [squareData.wkt],
  627. mianji: squareData.area,
  628. };
  629. // 绘制默认地块
  630. indexMap.setAreaGeometry(geometryData.geometryArr);
  631. // 保存地块数据
  632. polygonArr.value = geometryData.geometryArr;
  633. // 标记已创建默认地块
  634. hasDefaultPolygon.value = true;
  635. // 不跳转,停留在当前页面
  636. return;
  637. }
  638. }
  639. // 如果已有默认地块,则跳转到编辑页面
  640. // 保存到store中以便在编辑页面回显
  641. if (polygonArr.value) {
  642. const polygonData = {
  643. geometryArr: polygonArr.value,
  644. mianji: ruleForm.mu || "", // 保存用户输入的面积,如果没有输入则为空
  645. isConfirmed: false, // 标记:还未从编辑页面确认返回
  646. };
  647. store.commit("home/SET_FARM_POLYGON", polygonData);
  648. }
  649. router.push(
  650. `/edit_map?mapCenter=${centerPoint.value}&pointName=${ruleForm.address}&pointAddress=${pointAddress.value}&from=${route.query.from}&type=${route.query.type}`
  651. );
  652. }
  653. const pointAddress = ref(null);
  654. const farmCity = ref(null);
  655. function getLocationName(location) {
  656. const params = {
  657. key: MAP_KEY,
  658. location,
  659. };
  660. VE_API.old_mini_map.location(params).then(({ result }) => {
  661. // locationVal.value = result.formatted_addresses.recommend;
  662. const add = result.formatted_addresses?.recommend ? result.formatted_addresses.recommend : result.address + "";
  663. ruleForm.address = add;
  664. pointAddress.value = result.address;
  665. farmCity.value = result.address_component?.city + result.address_component?.district || "";
  666. // 地址修改后,如果满足条件则自动更新农场名称
  667. updateFarmNameIfNeeded();
  668. });
  669. }
  670. watch(
  671. () => mapLocation.data,
  672. (newValue, oldValue) => {
  673. if (newValue) {
  674. let { latitude, longitude } = transformFromWGSToGCJ(newValue[1], newValue[0]);
  675. centerPoint.value = `POINT (${newValue[0]} ${newValue[1]})`;
  676. getLocationName(`${latitude},${longitude}`);
  677. }
  678. }
  679. );
  680. const specieList = ref([]);
  681. function getSpecieList() {
  682. return VE_API.farm.fetchSpecieList({ point: centerPoint.value }).then(({ data }) => {
  683. specieList.value = data || [];
  684. return data;
  685. });
  686. }
  687. async function changeSpecie(v) {
  688. // const data = await checkExistsEnabledScheme(v.defaultContainerId);
  689. // if (!data) {
  690. // ElMessage.warning("该品类暂无可用方案,请选择其他品类");
  691. // ruleForm.speciesItem = "";
  692. // ruleForm.typeId = "";
  693. // fruitsList.value = [];
  694. // return;
  695. // }
  696. getFruitsTypeItemList(v.id);
  697. // 清空品种选择
  698. ruleForm.typeId = "";
  699. // 只有在创建模式下且用户没有手动修改过农场名称时,才自动设置农场名称
  700. if (route.query.type !== "edit" && !isFarmNameManuallyModified.value && farmCity.value) {
  701. ruleForm.name = farmCity.value + v.name + "农场";
  702. }
  703. }
  704. //判断是否存在可用方案
  705. async function checkExistsEnabledScheme(containerId) {
  706. const { data } = await VE_API.home.existsEnabledScheme({containerId});
  707. return data;
  708. }
  709. const fruitsList = ref([]);
  710. function getFruitsTypeItemList(parentId) {
  711. VE_API.farm.fruitsTypeItemList({ parentId }).then(({ data }) => {
  712. fruitsList.value = data;
  713. });
  714. }
  715. /**
  716. * 格式化日期为 YYYY-MM-DD 格式
  717. * @param {Date} date - 日期对象
  718. * @returns {string} 格式化后的日期字符串
  719. */
  720. function formatDate(date) {
  721. const year = date.getFullYear();
  722. const month = String(date.getMonth() + 1).padStart(2, "0");
  723. const day = String(date.getDate()).padStart(2, "0");
  724. return `${year}-${month}-${day}`;
  725. }
  726. function backgToHome() {
  727. ruleFormRef.value?.resetFields();
  728. // 根据来源页面决定返回目标
  729. const fromPage = route.query.from;
  730. if (route.query.miniJson) {
  731. const json = JSON.parse(route.query.miniJson);
  732. if (json.isMini) {
  733. const dropdownGardenItem = ref({
  734. organId: json.farmId,
  735. periodId: json.periodId,
  736. name: json.name,
  737. page: "create_farm",
  738. showFarmSelect: true,
  739. images: json.images,
  740. });
  741. wx.miniProgram.reLaunch({
  742. url: `/pages/subPages/new_recognize/index?gardenData=${JSON.stringify(dropdownGardenItem.value)}`,
  743. });
  744. }
  745. } else {
  746. if (fromPage && fromPage !== "details") {
  747. if(route.query.type === "farmer") {
  748. router.go(-1);
  749. } else {
  750. router.replace(`/${fromPage}`);
  751. }
  752. return;
  753. } else {
  754. router.go(-1);
  755. }
  756. }
  757. }
  758. // 处理面积按键输入 - 只允许数字和小数点
  759. function handleMianjiKeypress(event) {
  760. // 如果是从编辑地图返回的,不允许手动修改
  761. if (isFromEditMap.value) {
  762. event.preventDefault();
  763. return;
  764. }
  765. const charCode = event.which ? event.which : event.keyCode;
  766. const char = String.fromCharCode(charCode);
  767. // 允许数字 (0-9) 和小数点 (.)
  768. if (!/^[0-9.]$/.test(char)) {
  769. event.preventDefault();
  770. return;
  771. }
  772. // 如果已经有小数点,不允许再输入小数点
  773. const currentValue = ruleForm.mu || "";
  774. if (char === "." && currentValue.includes(".")) {
  775. event.preventDefault();
  776. return;
  777. }
  778. // 不允许以小数点开头
  779. if (char === "." && !currentValue) {
  780. event.preventDefault();
  781. return;
  782. }
  783. }
  784. // 处理农场名称输入
  785. function handleFarmNameInput() {
  786. // 标记用户已手动修改过农场名称
  787. isFarmNameManuallyModified.value = true;
  788. }
  789. // 根据地址更新农场名称(如果满足条件)
  790. function updateFarmNameIfNeeded() {
  791. // 只有在创建模式下且用户没有手动修改过农场名称且已选择品类时,才自动更新农场名称
  792. if (route.query.type !== "edit" && !isFarmNameManuallyModified.value && ruleForm.speciesItem && farmCity.value) {
  793. ruleForm.name = farmCity.value + ruleForm.speciesItem.name + "农场";
  794. }
  795. }
  796. // 回填编辑数据
  797. function populateEditData() {
  798. const editData = store.state.home.editFarmData;
  799. if (!editData) {
  800. return;
  801. }
  802. // 回填基本信息
  803. ruleForm.name = editData.name || "";
  804. ruleForm.fzr = editData.fzr || "";
  805. ruleForm.tel = editData.tel || "";
  806. ruleForm.defaultFarm = editData.defaultOption || false;
  807. ruleForm.mu = editData.mianji || "";
  808. ruleForm.address = editData.address || "";
  809. // 设置地图中心点
  810. if (editData.pointWkt) {
  811. centerPoint.value = editData.pointWkt;
  812. const arr = convertPointToArray(editData.pointWkt);
  813. indexMap.setMapPosition(arr);
  814. }
  815. // 处理地块数据
  816. if (editData.geomWkt) {
  817. polygonArr.value = [editData.geomWkt];
  818. indexMap.setAreaGeometry([editData.geomWkt]);
  819. hasDefaultPolygon.value = true;
  820. isFromEditMap.value = true; // 编辑模式下锁定面积输入
  821. }
  822. // 处理作物数据
  823. if (editData.speciesId && editData.speciesName) {
  824. // 设置品类 - 需要匹配模板中的数据结构
  825. ruleForm.speciesItem = {
  826. value: editData.speciesId,
  827. id: editData.speciesId,
  828. name: editData.speciesName,
  829. defaultContainerId: editData.containerId,
  830. };
  831. // 获取品种列表
  832. getFruitsTypeItemList(editData.speciesId);
  833. // 设置品种
  834. if (editData.typeId) {
  835. ruleForm.typeId = editData.typeId;
  836. }
  837. }
  838. // 设置地址信息
  839. if (editData.district) {
  840. try {
  841. const districtInfo = JSON.parse(editData.district);
  842. pointAddress.value = districtInfo.province + districtInfo.city + districtInfo.district;
  843. } catch (e) {
  844. console.warn("解析地址信息失败:", e);
  845. }
  846. }
  847. }
  848. // 处理面积输入
  849. let mianjiInputTimer = null;
  850. function handleMianjiInput(value) {
  851. // 如果是从编辑地图返回的,不允许手动修改
  852. if (isFromEditMap.value) {
  853. return;
  854. }
  855. // 过滤非法字符,只保留数字和小数点
  856. let filteredValue = value.replace(/[^\d.]/g, "");
  857. // 确保只有一个小数点
  858. const parts = filteredValue.split(".");
  859. if (parts.length > 2) {
  860. filteredValue = parts[0] + "." + parts.slice(1).join("");
  861. }
  862. // 限制小数点后最多2位
  863. if (parts.length === 2 && parts[1].length > 2) {
  864. filteredValue = parts[0] + "." + parts[1].substring(0, 2);
  865. }
  866. // 更新输入框的值(如果被过滤了)
  867. if (filteredValue !== value) {
  868. ruleForm.mu = filteredValue;
  869. return;
  870. }
  871. // 清除之前的定时器
  872. if (mianjiInputTimer) {
  873. clearTimeout(mianjiInputTimer);
  874. }
  875. // 防抖处理,用户停止输入500ms后再更新地块
  876. mianjiInputTimer = setTimeout(() => {
  877. const mu = parseFloat(filteredValue);
  878. // 验证输入的有效性
  879. if (!mu || isNaN(mu) || mu <= 0) {
  880. return;
  881. }
  882. // 根据亩数重新生成地块
  883. if (centerPoint.value) {
  884. const arr = convertPointToArray(centerPoint.value);
  885. const squareData = generateSquarePolygonByMu(arr, mu);
  886. const geometryData = {
  887. geometryArr: [squareData.wkt],
  888. mianji: squareData.area,
  889. };
  890. // 清除旧地块
  891. indexMap.clearLayer();
  892. // 绘制新地块
  893. indexMap.setAreaGeometry(geometryData.geometryArr);
  894. // 更新状态
  895. polygonArr.value = geometryData.geometryArr;
  896. // 标记已创建默认地块
  897. hasDefaultPolygon.value = true;
  898. }
  899. // 如果路由type是farmer且亩数大于等于50,自动调用toSubPage方法
  900. if (route.query.type === 'farmer' && mu >= 50) {
  901. toSubPage();
  902. }
  903. }, 500);
  904. }
  905. </script>
  906. <style lang="scss" scoped>
  907. ::v-deep {
  908. .el-form-item--default {
  909. margin-bottom: 20px;
  910. }
  911. }
  912. .create-farm {
  913. position: relative;
  914. width: 100%;
  915. height: 100vh;
  916. overflow: hidden;
  917. .map-container {
  918. width: 100%;
  919. height: calc(100% - 320px);
  920. }
  921. .checkbox {
  922. padding: 0 12px 6px;
  923. font-size: 15px;
  924. }
  925. .farm-content {
  926. position: absolute;
  927. top: 40px;
  928. left: 0;
  929. width: 100%;
  930. height: calc(100% - 40px);
  931. pointer-events: none;
  932. z-index: 2;
  933. .top-mask {
  934. height: 100px;
  935. position: absolute;
  936. z-index: 2;
  937. top: 0;
  938. left: 0;
  939. width: 100%;
  940. background: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%);
  941. }
  942. }
  943. .farm-filter {
  944. pointer-events: all;
  945. margin: 12px;
  946. position: relative;
  947. z-index: 10;
  948. background: rgba(0, 0, 0, 0.3);
  949. border-radius: 20px;
  950. border: 1px solid rgba(255, 255, 255, 0.4);
  951. &::before {
  952. content: "";
  953. position: absolute;
  954. left: 12px;
  955. top: 9px;
  956. width: 14px;
  957. height: 14px;
  958. background: url("@/assets/img/home/search.png") no-repeat center center / 100% 100%;
  959. }
  960. ::v-deep {
  961. .el-select__wrapper {
  962. background: none;
  963. box-shadow: none;
  964. padding-left: 34px;
  965. font-size: 12px;
  966. .el-select__selected-item,
  967. .el-select__placeholder,
  968. .el-select__input {
  969. color: rgba(255, 255, 255, 0.6);
  970. &.is-transparent {
  971. color: #ccc;
  972. font-size: 12px;
  973. }
  974. }
  975. }
  976. .el-select {
  977. transition: all 0.3s;
  978. .el-input.is-focus .el-input__wrapper {
  979. box-shadow: none !important;
  980. }
  981. }
  982. .el-input {
  983. .el-input__wrapper {
  984. background: none;
  985. box-shadow: none;
  986. padding-left: 18px;
  987. font-size: 11px;
  988. .el-input__inner {
  989. color: rgba(255, 255, 255, 0.6);
  990. }
  991. &.is-focus {
  992. .el-input__inner {
  993. color: #ccc;
  994. font-size: 11px;
  995. }
  996. }
  997. }
  998. }
  999. }
  1000. }
  1001. .create-wrap {
  1002. position: absolute;
  1003. bottom: 0px;
  1004. left: 0;
  1005. width: 100%;
  1006. background: linear-gradient(180deg, transparent 0%, #f5f7fb 20%);
  1007. }
  1008. .create-box {
  1009. pointer-events: all;
  1010. margin: 0 12px 10px 12px;
  1011. width: calc(100% - 24px);
  1012. background: #e0f1fe;
  1013. border-radius: 14px;
  1014. .box-content {
  1015. position: relative;
  1016. &::after {
  1017. position: absolute;
  1018. right: 10px;
  1019. top: 2px;
  1020. content: "";
  1021. width: 79px;
  1022. height: 72px;
  1023. background: url("@/assets/img/home/creat-bg.png") no-repeat center / 100% 100%;
  1024. }
  1025. }
  1026. .create-title {
  1027. display: flex;
  1028. align-items: center;
  1029. padding: 12px 6px 12px 12px;
  1030. color: #0089f5;
  1031. font-size: 18px;
  1032. font-weight: bold;
  1033. .title-icon {
  1034. width: 18px;
  1035. padding-right: 10px;
  1036. }
  1037. }
  1038. .create-content {
  1039. background: #fff;
  1040. border-radius: 14px;
  1041. padding: 12px;
  1042. position: relative;
  1043. z-index: 2;
  1044. .create-from {
  1045. .select-wrap {
  1046. display: flex;
  1047. // width: 86%;
  1048. ::v-deep {
  1049. .el-input__wrapper {
  1050. background: none;
  1051. box-shadow: none;
  1052. }
  1053. .el-input__inner {
  1054. font-size: 14px;
  1055. color: rgba(0, 0, 0, 0.5);
  1056. }
  1057. .el-select__wrapper {
  1058. background: none;
  1059. box-shadow: none;
  1060. gap: 2px;
  1061. padding: 4px 2px;
  1062. justify-content: center;
  1063. }
  1064. .el-select__selection {
  1065. flex: none;
  1066. width: fit-content;
  1067. }
  1068. .el-select__placeholder {
  1069. color: #000;
  1070. position: static;
  1071. transform: none;
  1072. width: fit-content;
  1073. }
  1074. }
  1075. &.client-wrap {
  1076. ::v-deep {
  1077. .el-select__wrapper {
  1078. justify-content: flex-start;
  1079. }
  1080. }
  1081. }
  1082. // .select-item {
  1083. // width: fit-content;
  1084. // }
  1085. .period-select {
  1086. margin-left: 6px;
  1087. }
  1088. .select-item {
  1089. min-width: 76px;
  1090. }
  1091. }
  1092. ::v-deep {
  1093. .el-form-item__label {
  1094. color: #000;
  1095. }
  1096. .el-form-item__error {
  1097. top: 117%;
  1098. }
  1099. .el-form-item {
  1100. position: relative;
  1101. &::after {
  1102. content: "";
  1103. position: absolute;
  1104. left: 60px;
  1105. bottom: -5px;
  1106. width: calc(100% - 60px);
  1107. height: 1px;
  1108. background: rgba(0, 0, 0, 0.08);
  1109. }
  1110. }
  1111. .el-input__wrapper {
  1112. box-shadow: none;
  1113. padding: 1px 6px;
  1114. }
  1115. }
  1116. .area-box {
  1117. display: flex;
  1118. align-items: center;
  1119. width: 100%;
  1120. .unit {
  1121. padding-right: 10px;
  1122. }
  1123. }
  1124. .position-wrap {
  1125. display: flex;
  1126. justify-content: space-between;
  1127. align-items: center;
  1128. .draw-btn {
  1129. flex: none;
  1130. padding: 0 12px;
  1131. height: 30px;
  1132. line-height: 30px;
  1133. box-sizing: border-box;
  1134. color: #2199f8;
  1135. border: 1px solid #2199f8;
  1136. background: rgba(33, 153, 248, 0.1);
  1137. border-radius: 20px;
  1138. font-size: 12px;
  1139. }
  1140. }
  1141. }
  1142. .create-btn {
  1143. display: flex;
  1144. align-items: center;
  1145. width: 100%;
  1146. padding-top: 10px;
  1147. .btn-item {
  1148. flex: 1;
  1149. text-align: center;
  1150. padding: 0 11px;
  1151. height: 40px;
  1152. line-height: 40px;
  1153. border-radius: 34px;
  1154. font-size: 16px;
  1155. box-sizing: border-box;
  1156. &.sencond-btn {
  1157. border: 1px solid rgba(153, 153, 153, 0.5);
  1158. color: #666666;
  1159. }
  1160. &.primary-btn {
  1161. background: linear-gradient(180deg, #76c3ff, #2199f8);
  1162. color: #fff;
  1163. }
  1164. }
  1165. .btn-item + .btn-item {
  1166. margin-left: 5px;
  1167. }
  1168. }
  1169. }
  1170. }
  1171. }
  1172. </style>
  1173. <style lang="scss">
  1174. .location-search-popper {
  1175. .el-select-dropdown__list {
  1176. max-width: 96vw;
  1177. overflow-x: auto;
  1178. }
  1179. .sub-title {
  1180. padding-left: 6px;
  1181. font-size: 12px;
  1182. color: #ccc;
  1183. }
  1184. }
  1185. </style>