index.vue 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947
  1. <template>
  2. <div class="create-farm">
  3. <custom-header :name="paramsType === 'client' ? '新增用户' : paramsType === 'edit' ? '编辑农场': '创建农场'" :isGoBack="true" @goback="backgToHome"></custom-header>
  4. <!-- 地图 -->
  5. <div class="map-container" ref="mapContainer"></div>
  6. <div class="farm-content">
  7. <div class="top-mask"></div>
  8. <div class="farm-filter">
  9. <el-select
  10. v-model="locationVal"
  11. filterable
  12. remote
  13. reserve-keyword
  14. placeholder="搜索位置"
  15. :remote-method="remoteMethod"
  16. :loading="loading"
  17. @change="handleSearchRes"
  18. popper-class="location-search-popper"
  19. >
  20. <el-option
  21. v-for="(item, index) in locationOptions.list"
  22. :key="index"
  23. :label="item.title"
  24. :value="{ value: item.point, item }"
  25. >
  26. <span>{{ item.title }}</span>
  27. <span class="sub-title">{{ item.province }}{{ item.city }}{{ item.district }}</span>
  28. </el-option>
  29. </el-select>
  30. </div>
  31. <!-- 创建 -->
  32. <div class="create-wrap">
  33. <div class="create-box">
  34. <div class="box-content">
  35. <div class="create-title">
  36. <img class="title-icon" src="@/assets/img/home/create-icon.png" alt="" />
  37. {{ paramsType === 'client' ? "新增用户" : paramsType === 'edit' ?"编辑农场":"创建农场" }}
  38. </div>
  39. <div class="create-content">
  40. <div class="create-from">
  41. <el-form
  42. ref="ruleFormRef"
  43. :model="ruleForm"
  44. :rules="rules"
  45. class="demo-ruleForm"
  46. >
  47. <el-form-item label="农场位置" prop="address">
  48. <div class="position-wrap">
  49. <el-input
  50. placeholder="农场位置"
  51. readonly
  52. v-model="ruleForm.address"
  53. autocomplete="off"
  54. />
  55. <div class="draw-btn" @click="toSubPage">{{ hasDefaultPolygon ? '点击勾选地块' : '新增地块' }}</div>
  56. </div>
  57. </el-form-item>
  58. <el-form-item label="种植作物" prop="speciesItem">
  59. <div class="select-wrap">
  60. <el-select
  61. @change="changeSpecie"
  62. class="select-item"
  63. v-model="ruleForm.speciesItem"
  64. placeholder="作物类型"
  65. >
  66. <el-option
  67. v-for="(item, index) in specieList"
  68. :key="index"
  69. :label="item.name"
  70. :value="{ value: item.id, ...item }"
  71. />
  72. </el-select>
  73. <el-select
  74. v-model="ruleForm.phenologyId"
  75. placeholder="物候期"
  76. class="period-select select-item"
  77. >
  78. <el-option
  79. v-for="(item, index) in phenologyList"
  80. :key="index"
  81. :label="item.name"
  82. :value="item.id"
  83. />
  84. </el-select>
  85. </div>
  86. </el-form-item>
  87. <el-form-item label="联系人" prop="fzr">
  88. <div class="area-box">
  89. <el-input
  90. placeholder="请输入联系人姓名"
  91. v-model="ruleForm.fzr"
  92. autocomplete="off"
  93. style="width: fit-content"
  94. />
  95. </div>
  96. </el-form-item>
  97. <el-form-item label="联系电话" prop="tel">
  98. <div class="area-box">
  99. <el-input
  100. placeholder="请输入联系人电话"
  101. v-model="ruleForm.tel"
  102. autocomplete="off"
  103. style="width: fit-content"
  104. />
  105. </div>
  106. </el-form-item>
  107. <el-form-item label="农场面积" prop="mu">
  108. <div class="area-box">
  109. <el-input
  110. :placeholder="isFromEditMap ? '勾选地块获得农场面积' : '请输入亩数'"
  111. v-model="ruleForm.mu"
  112. :readonly="isFromEditMap"
  113. type="text"
  114. autocomplete="off"
  115. style="width: fit-content"
  116. @input="handleMianjiInput"
  117. @keypress="handleMianjiKeypress"
  118. />
  119. <div class="unit">亩</div>
  120. </div>
  121. </el-form-item>
  122. <el-form-item label="农场名称" prop="name">
  123. <el-input
  124. placeholder="请输入您的农场名称"
  125. v-model="ruleForm.name"
  126. autocomplete="off"
  127. />
  128. </el-form-item>
  129. </el-form>
  130. </div>
  131. <div class="create-btn">
  132. <div class="btn-item sencond-btn" @click="resetForm(ruleFormRef)">取消</div>
  133. <div class="btn-item primary-btn" @click="submitForm(ruleFormRef)">
  134. {{ paramsType === 'client' ? "添加" : paramsType === 'edit' ?"确认修改":"立即创建" }}
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. </div>
  142. </div>
  143. </template>
  144. <script setup>
  145. import customHeader from "@/components/customHeader.vue";
  146. import IndexMap from "./map/index.js";
  147. import { useRoute, useRouter } from "vue-router";
  148. import { mapLocation } from "./map/index.js";
  149. import { onMounted, ref, reactive, watch, onActivated } from "vue";
  150. import { useStore } from "vuex";
  151. import { convertPointToArray } from "@/utils/index";
  152. import { ElMessage } from "element-plus";
  153. import { transformFromGCJToWGS, transformFromWGSToGCJ } from "@/utils/WSCoordinate.js";
  154. const route = useRoute();
  155. const router = useRouter();
  156. const store = useStore();
  157. const indexMap = new IndexMap();
  158. const mapContainer = ref(null);
  159. // 标记是否从编辑地图页面确认返回
  160. const isFromEditMap = ref(false);
  161. // 标记是否已创建默认地块
  162. const hasDefaultPolygon = ref(false);
  163. /**
  164. * 根据中心点生成指定边长的正方形地块WKT
  165. * @param {Array} center - 中心点坐标 [lng, lat]
  166. * @param {Number} sideLength - 正方形边长(米)
  167. * @returns {Object} 包含WKT字符串和面积(亩)的对象
  168. */
  169. function generateSquarePolygonBySideLength(center, sideLength) {
  170. // 确保输入是数字类型
  171. const lng = parseFloat(center[0]);
  172. const lat = parseFloat(center[1]);
  173. // 半边长(米)
  174. const halfSide = sideLength / 2;
  175. // 纬度方向:1度约等于111000米
  176. const latDelta = halfSide / 111000;
  177. // 经度方向:需要根据纬度调整,1度约等于111000 * cos(lat)米
  178. const lngDelta = halfSide / (111000 * Math.cos(lat * Math.PI / 180));
  179. // 计算四个顶点(逆时针顺序)
  180. const topLeft = [lng - lngDelta, lat + latDelta];
  181. const topRight = [lng + lngDelta, lat + latDelta];
  182. const bottomRight = [lng + lngDelta, lat - latDelta];
  183. const bottomLeft = [lng - lngDelta, lat - latDelta];
  184. // 生成MULTIPOLYGON格式的WKT(需要闭合,所以第一个点要重复)
  185. // 明确使用join来格式化坐标点
  186. const formatPoint = (point) => `${point[0]} ${point[1]}`;
  187. const coordinates = [topLeft, topRight, bottomRight, bottomLeft, topLeft]
  188. .map(formatPoint)
  189. .join(', ');
  190. // 注意:MULTIPOLYGON 和括号之间需要有一个空格
  191. const wkt = `MULTIPOLYGON (((${coordinates})))`;
  192. // 计算面积(亩): 边长200米 = 40,000平方米 ≈ 60亩
  193. const areaInSquareMeters = sideLength * sideLength;
  194. const areaInMu = (areaInSquareMeters / 666.67).toFixed(2);
  195. return { wkt, area: areaInMu };
  196. }
  197. /**
  198. * 根据中心点和亩数生成正方形地块WKT
  199. * @param {Array} center - 中心点坐标 [lng, lat]
  200. * @param {Number} mu - 面积(亩)
  201. * @returns {Object} 包含WKT字符串和面积(亩)的对象
  202. */
  203. function generateSquarePolygonByMu(center, mu) {
  204. // 确保输入是数字类型
  205. const lng = parseFloat(center[0]);
  206. const lat = parseFloat(center[1]);
  207. // 1亩 ≈ 666.67平方米
  208. const areaInSquareMeters = mu * 666.67;
  209. // 正方形边长(米)
  210. const sideLength = Math.sqrt(areaInSquareMeters);
  211. // 半边长(米)
  212. const halfSide = sideLength / 2;
  213. // 纬度方向:1度约等于111000米
  214. const latDelta = halfSide / 111000;
  215. // 经度方向:需要根据纬度调整,1度约等于111000 * cos(lat)米
  216. const lngDelta = halfSide / (111000 * Math.cos(lat * Math.PI / 180));
  217. // 计算四个顶点(逆时针顺序)
  218. const topLeft = [lng - lngDelta, lat + latDelta];
  219. const topRight = [lng + lngDelta, lat + latDelta];
  220. const bottomRight = [lng + lngDelta, lat - latDelta];
  221. const bottomLeft = [lng - lngDelta, lat - latDelta];
  222. // 生成MULTIPOLYGON格式的WKT(需要闭合,所以第一个点要重复)
  223. const formatPoint = (point) => `${point[0]} ${point[1]}`;
  224. const coordinates = [topLeft, topRight, bottomRight, bottomLeft, topLeft]
  225. .map(formatPoint)
  226. .join(', ');
  227. // 注意:MULTIPOLYGON 和括号之间需要有一个空格
  228. const wkt = `MULTIPOLYGON (((${coordinates})))`;
  229. return { wkt, area: parseFloat(mu).toFixed(2) };
  230. }
  231. onMounted(() => {
  232. // 清除上一次的地块数据,确保每次进入都是全新状态
  233. store.commit("home/SET_FARM_POLYGON", null);
  234. isFromEditMap.value = false; // 初始化时可以手动输入
  235. hasDefaultPolygon.value = false; // 初始化时没有默认地块
  236. centerPoint.value = store.state.home.miniUserLocationPoint;
  237. const arr = convertPointToArray(centerPoint.value);
  238. getLocationName(`${arr[1]},${arr[0]}`);
  239. indexMap.initMap(centerPoint.value, mapContainer.value);
  240. // 不再初始化时绘制默认地块,等待用户点击"新增地块"按钮
  241. // 清空地块和面积数据
  242. polygonArr.value = null;
  243. ruleForm.mu = '';
  244. getSpecieList();
  245. });
  246. const polygonArr = ref(null);
  247. const paramsType = ref(null);
  248. onActivated(() => {
  249. paramsType.value = route.query.type;
  250. // 如果是从 home 页面进入,重置所有数据
  251. if (route.query.isFromHome) {
  252. // 清除旧的地块数据
  253. store.commit("home/SET_FARM_POLYGON", null);
  254. isFromEditMap.value = false; // 从home进入,可以手动输入
  255. hasDefaultPolygon.value = false; // 重置默认地块状态
  256. centerPoint.value = store.state.home.miniUserLocationPoint;
  257. const arr = convertPointToArray(centerPoint.value);
  258. getLocationName(`${arr[1]},${arr[0]}`);
  259. indexMap.setMapPosition(arr);
  260. indexMap.clearLayer();
  261. // 不再自动生成默认地块,等待用户点击"新增地块"
  262. polygonArr.value = null;
  263. ruleForm.mu = '';
  264. return; // 直接返回,不执行下面的逻辑
  265. }
  266. indexMap.clearLayer();
  267. // 绘制勾画范围(从edit_map返回的情况)
  268. const polygonData = store.state.home.polygonData;
  269. if (polygonData) {
  270. // 检查地块数据是否有效(数组存在且不为空)
  271. const hasValidGeometry = polygonData.geometryArr &&
  272. Array.isArray(polygonData.geometryArr) &&
  273. polygonData.geometryArr.length > 0;
  274. if (hasValidGeometry) {
  275. // 用户从edit_map返回,且有有效的地块数据
  276. // 根据 isConfirmed 判断是否锁定输入框
  277. if (polygonData.isConfirmed) {
  278. // 用户点击了"确认"按钮,锁定输入框并显示精确面积
  279. isFromEditMap.value = true;
  280. ruleForm.mu = polygonData.mianji;
  281. } else {
  282. // 用户点击了"取消",不锁定输入框,面积保持原样
  283. isFromEditMap.value = false;
  284. // 面积输入框保持之前的值,不更新
  285. }
  286. indexMap.setAreaGeometry(polygonData.geometryArr);
  287. polygonArr.value = polygonData.geometryArr;
  288. // 有地块数据时,标记已创建默认地块
  289. hasDefaultPolygon.value = true;
  290. } else {
  291. // 用户在编辑页面删除了所有地块
  292. // 重置所有状态
  293. polygonArr.value = null;
  294. ruleForm.mu = '';
  295. isFromEditMap.value = false;
  296. hasDefaultPolygon.value = false;
  297. }
  298. } else if (centerPoint.value && polygonArr.value) {
  299. // 没有新的编辑数据,保持当前地块
  300. // 同样需要检查数据有效性
  301. if (Array.isArray(polygonArr.value) && polygonArr.value.length > 0) {
  302. indexMap.setAreaGeometry(polygonArr.value);
  303. // 保持 hasDefaultPolygon 状态
  304. }
  305. }
  306. });
  307. // 搜索
  308. const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
  309. const locationVal = ref(null);
  310. const locationOptions = reactive({
  311. list: [],
  312. });
  313. const loading = ref(false);
  314. const remoteMethod = async (keyword) => {
  315. if (keyword) {
  316. locationOptions.list = [];
  317. loading.value = true;
  318. const params = {
  319. key: MAP_KEY,
  320. keyword,
  321. location: route.query.userLocation || "113.61702297075017,23.584863449735067",
  322. };
  323. await VE_API.old_mini_map.getCtiyList({ word: keyword }).then(({ data }) => {
  324. if (data && data.length) {
  325. data.forEach((item) => {
  326. item.point = item.location.lat + "," + item.location.lng;
  327. locationOptions.list.push(item);
  328. });
  329. }
  330. });
  331. VE_API.old_mini_map.search(params).then(({ data }) => {
  332. loading.value = false;
  333. data.forEach((item) => {
  334. item.point = item.location.lat + "," + item.location.lng;
  335. locationOptions.list.push(item);
  336. });
  337. });
  338. } else {
  339. locationOptions.list = [];
  340. }
  341. };
  342. const handleSearchRes = (v) => {
  343. const parts = v.value.split(",");
  344. let { latitude, longitude } = transformFromGCJToWGS(parseFloat(parts[0]), parseFloat(parts[1]));
  345. const coordinateArray = [longitude, latitude];
  346. indexMap.setMapPosition(coordinateArray);
  347. centerPoint.value = `POINT (${coordinateArray[0]} ${coordinateArray[1]})`;
  348. ruleForm.address = v.item?.title || v.item?.address;
  349. pointAddress.value = v.item?.province + v.item?.city + v.item?.district;
  350. };
  351. // 表单
  352. const ruleFormRef = ref(null);
  353. const ruleForm = reactive({
  354. address: "",
  355. mu: "",
  356. speciesItem: null,
  357. phenologyId: "",
  358. name: "",
  359. fzr: "",
  360. tel: "",
  361. });
  362. // 自定义验证规则:验证面积必须是大于0的数字
  363. const validateMianji = (rule, value, callback) => {
  364. if (!value) {
  365. callback(new Error('请输入农场面积'));
  366. } else {
  367. const num = parseFloat(value);
  368. if (isNaN(num)) {
  369. callback(new Error('面积必须是数字'));
  370. } else if (num <= 0) {
  371. callback(new Error('面积必须大于0'));
  372. } else {
  373. callback();
  374. }
  375. }
  376. };
  377. const rules = reactive({
  378. address: [{ required: true, message: "请选择农场位置", trigger: "blur" }],
  379. mu: [
  380. { required: true, message: "请输入农场面积", trigger: "blur" },
  381. { validator: validateMianji, trigger: ["blur", "change"] }
  382. ],
  383. speciesItem: [{ required: true, message: "请选择品类", trigger: "blur" }],
  384. phenologyId: [{ required: true, message: "请选择物候期", trigger: "blur" }],
  385. name: [{ required: true, message: "请输入您的农场名称", trigger: "blur" }],
  386. fzr: [{ required: true, message: "请输入联系人姓名", trigger: "blur" }],
  387. tel: [
  388. { required: true, message: "请输入联系人电话", trigger: "blur" },
  389. {
  390. pattern: /^1[3-9]\d{9}$/,
  391. message: "请输入正确的手机号码",
  392. trigger: "blur"
  393. }
  394. ],
  395. });
  396. const submitForm = (formEl) => {
  397. if (!formEl) return;
  398. formEl.validate((valid) => {
  399. if (valid) {
  400. const params = {
  401. ...ruleForm,
  402. wkt: centerPoint.value,
  403. speciesId: ruleForm.speciesItem?.id,
  404. containerId: ruleForm.speciesItem?.defaultContainerId,
  405. geom: polygonArr.value,
  406. };
  407. VE_API.farm.saveFarm(params).then((res) => {
  408. ElMessage.success("创建成功");
  409. // 重置表单和地块数据
  410. ruleFormRef.value.resetFields();
  411. store.commit("home/SET_FARM_POLYGON", null);
  412. polygonArr.value = null;
  413. isFromEditMap.value = false;
  414. // 返回首页并显示成功弹窗
  415. router.replace("/home?reload=true&showSuccess=true");
  416. });
  417. console.log("submit!", params);
  418. } else {
  419. console.log("error submit!");
  420. }
  421. });
  422. };
  423. const resetForm = (formEl) => {
  424. if (!formEl) return;
  425. formEl.resetFields();
  426. // 清除地块数据
  427. store.commit("home/SET_FARM_POLYGON", null);
  428. polygonArr.value = null;
  429. isFromEditMap.value = false;
  430. hasDefaultPolygon.value = false; // 重置默认地块状态
  431. if(route.query.isFromHome) {
  432. router.replace("/home?reload=true");
  433. return;
  434. }
  435. router.go(-1)
  436. };
  437. const centerPoint = ref(null);
  438. function toSubPage() {
  439. // 如果还没有默认地块,先创建默认地块
  440. if (!hasDefaultPolygon.value) {
  441. if (centerPoint.value) {
  442. const arr = convertPointToArray(centerPoint.value);
  443. const squareData = generateSquarePolygonBySideLength(arr, 200); // 200米边长
  444. const geometryData = {
  445. geometryArr: [squareData.wkt],
  446. mianji: squareData.area,
  447. };
  448. // 绘制默认地块
  449. indexMap.setAreaGeometry(geometryData.geometryArr);
  450. // 保存地块数据
  451. polygonArr.value = geometryData.geometryArr;
  452. // 标记已创建默认地块
  453. hasDefaultPolygon.value = true;
  454. // 不跳转,停留在当前页面
  455. return;
  456. }
  457. }
  458. // 如果已有默认地块,则跳转到编辑页面
  459. // 保存到store中以便在编辑页面回显
  460. if (polygonArr.value) {
  461. const polygonData = {
  462. geometryArr: polygonArr.value,
  463. mianji: ruleForm.mu || '', // 保存用户输入的面积,如果没有输入则为空
  464. isConfirmed: false, // 标记:还未从编辑页面确认返回
  465. };
  466. store.commit("home/SET_FARM_POLYGON", polygonData);
  467. }
  468. router.push(
  469. `/edit_map?mapCenter=${centerPoint.value}&pointName=${ruleForm.address}&pointAddress=${pointAddress.value}`
  470. );
  471. }
  472. const pointAddress = ref(null);
  473. function getLocationName(location) {
  474. const params = {
  475. key: MAP_KEY,
  476. location,
  477. };
  478. VE_API.old_mini_map.location(params).then(({ result }) => {
  479. // locationVal.value = result.formatted_addresses.recommend;
  480. const add = result.formatted_addresses?.recommend ? result.formatted_addresses.recommend : result.address + "";
  481. ruleForm.address = add;
  482. pointAddress.value = result.address;
  483. });
  484. }
  485. watch(
  486. () => mapLocation.data,
  487. (newValue, oldValue) => {
  488. if (newValue) {
  489. let { latitude, longitude } = transformFromWGSToGCJ(newValue[1], newValue[0]);
  490. centerPoint.value = `POINT (${newValue[0]} ${newValue[1]})`;
  491. getLocationName(`${latitude},${longitude}`);
  492. }
  493. }
  494. );
  495. const specieList = ref([]);
  496. function getSpecieList() {
  497. VE_API.farm.fetchSpecieList().then(({ data }) => {
  498. specieList.value = data;
  499. });
  500. }
  501. function changeSpecie(v) {
  502. getPhenology(v.defaultContainerId);
  503. }
  504. const phenologyList = ref([]);
  505. function getPhenology(containerId) {
  506. VE_API.farm.fetchPhenologyList({ containerId }).then(({ data }) => {
  507. phenologyList.value = data;
  508. });
  509. }
  510. function backgToHome() {
  511. ruleFormRef.value?.resetFields();
  512. if(route.query.isFromHome) {
  513. router.replace("/home?reload=true");
  514. return;
  515. }
  516. router.go(-1)
  517. }
  518. // 处理面积按键输入 - 只允许数字和小数点
  519. function handleMianjiKeypress(event) {
  520. // 如果是从编辑地图返回的,不允许手动修改
  521. if (isFromEditMap.value) {
  522. event.preventDefault();
  523. return;
  524. }
  525. const charCode = event.which ? event.which : event.keyCode;
  526. const char = String.fromCharCode(charCode);
  527. // 允许数字 (0-9) 和小数点 (.)
  528. if (!/^[0-9.]$/.test(char)) {
  529. event.preventDefault();
  530. return;
  531. }
  532. // 如果已经有小数点,不允许再输入小数点
  533. const currentValue = ruleForm.mu || '';
  534. if (char === '.' && currentValue.includes('.')) {
  535. event.preventDefault();
  536. return;
  537. }
  538. // 不允许以小数点开头
  539. if (char === '.' && !currentValue) {
  540. event.preventDefault();
  541. return;
  542. }
  543. }
  544. // 处理面积输入
  545. let mianjiInputTimer = null;
  546. function handleMianjiInput(value) {
  547. // 如果是从编辑地图返回的,不允许手动修改
  548. if (isFromEditMap.value) {
  549. return;
  550. }
  551. // 过滤非法字符,只保留数字和小数点
  552. let filteredValue = value.replace(/[^\d.]/g, '');
  553. // 确保只有一个小数点
  554. const parts = filteredValue.split('.');
  555. if (parts.length > 2) {
  556. filteredValue = parts[0] + '.' + parts.slice(1).join('');
  557. }
  558. // 限制小数点后最多2位
  559. if (parts.length === 2 && parts[1].length > 2) {
  560. filteredValue = parts[0] + '.' + parts[1].substring(0, 2);
  561. }
  562. // 更新输入框的值(如果被过滤了)
  563. if (filteredValue !== value) {
  564. ruleForm.mu = filteredValue;
  565. return;
  566. }
  567. // 清除之前的定时器
  568. if (mianjiInputTimer) {
  569. clearTimeout(mianjiInputTimer);
  570. }
  571. // 防抖处理,用户停止输入500ms后再更新地块
  572. mianjiInputTimer = setTimeout(() => {
  573. const mu = parseFloat(filteredValue);
  574. // 验证输入的有效性
  575. if (!mu || isNaN(mu) || mu <= 0) {
  576. return;
  577. }
  578. // 根据亩数重新生成地块
  579. if (centerPoint.value) {
  580. const arr = convertPointToArray(centerPoint.value);
  581. const squareData = generateSquarePolygonByMu(arr, mu);
  582. const geometryData = {
  583. geometryArr: [squareData.wkt],
  584. mianji: squareData.area,
  585. };
  586. // 清除旧地块
  587. indexMap.clearLayer();
  588. // 绘制新地块
  589. indexMap.setAreaGeometry(geometryData.geometryArr);
  590. // 更新状态
  591. polygonArr.value = geometryData.geometryArr;
  592. // 标记已创建默认地块
  593. hasDefaultPolygon.value = true;
  594. }
  595. }, 500);
  596. }
  597. </script>
  598. <style lang="scss" scoped>
  599. ::v-deep{
  600. .el-form-item--default{
  601. margin-bottom: 20px;
  602. }
  603. }
  604. .create-farm {
  605. position: relative;
  606. width: 100%;
  607. height: 100vh;
  608. overflow: hidden;
  609. .map-container {
  610. width: 100%;
  611. height: calc(100% - 240px);
  612. }
  613. .farm-content {
  614. position: absolute;
  615. top: 40px;
  616. left: 0;
  617. width: 100%;
  618. height: calc(100% - 40px);
  619. pointer-events: none;
  620. z-index: 2;
  621. .top-mask {
  622. height: 100px;
  623. position: absolute;
  624. z-index: 2;
  625. top: 0;
  626. left: 0;
  627. width: 100%;
  628. background: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%);
  629. }
  630. }
  631. .farm-filter {
  632. pointer-events: all;
  633. margin: 12px;
  634. position: relative;
  635. z-index: 10;
  636. background: rgba(0, 0, 0, 0.3);
  637. border-radius: 20px;
  638. border: 1px solid rgba(255, 255, 255, 0.4);
  639. &::before {
  640. content: "";
  641. position: absolute;
  642. left: 12px;
  643. top: 9px;
  644. width: 14px;
  645. height: 14px;
  646. background: url("@/assets/img/home/search.png") no-repeat center center / 100% 100%;
  647. }
  648. ::v-deep {
  649. .el-select__wrapper {
  650. background: none;
  651. box-shadow: none;
  652. padding-left: 34px;
  653. font-size: 12px;
  654. .el-select__selected-item,
  655. .el-select__placeholder,
  656. .el-select__input {
  657. color: rgba(255, 255, 255, 0.6);
  658. &.is-transparent {
  659. color: #ccc;
  660. font-size: 12px;
  661. }
  662. }
  663. }
  664. .el-select {
  665. transition: all 0.3s;
  666. .el-input.is-focus .el-input__wrapper {
  667. box-shadow: none !important;
  668. }
  669. }
  670. .el-input {
  671. .el-input__wrapper {
  672. background: none;
  673. box-shadow: none;
  674. padding-left: 18px;
  675. font-size: 11px;
  676. .el-input__inner {
  677. color: rgba(255, 255, 255, 0.6);
  678. }
  679. &.is-focus {
  680. .el-input__inner {
  681. color: #ccc;
  682. font-size: 11px;
  683. }
  684. }
  685. }
  686. }
  687. }
  688. }
  689. .create-wrap {
  690. position: absolute;
  691. bottom: 0px;
  692. left: 0;
  693. width: 100%;
  694. background: linear-gradient(180deg, transparent 0%, #f5f7fb 20%);
  695. }
  696. .create-box {
  697. pointer-events: all;
  698. margin: 0 12px 10px 12px;
  699. width: calc(100% - 24px);
  700. background: #e0f1fe;
  701. border-radius: 14px;
  702. .box-content {
  703. position: relative;
  704. &::after {
  705. position: absolute;
  706. right: 10px;
  707. top: 2px;
  708. content: "";
  709. width: 79px;
  710. height: 72px;
  711. background: url("@/assets/img/home/creat-bg.png") no-repeat center / 100% 100%;
  712. }
  713. }
  714. .create-title {
  715. display: flex;
  716. align-items: center;
  717. padding: 12px 6px 12px 12px;
  718. color: #0089f5;
  719. font-size: 18px;
  720. font-weight: bold;
  721. .title-icon {
  722. width: 18px;
  723. padding-right: 10px;
  724. }
  725. }
  726. .create-content {
  727. background: #fff;
  728. border-radius: 14px;
  729. padding: 12px;
  730. position: relative;
  731. z-index: 2;
  732. .create-from {
  733. .select-wrap {
  734. display: flex;
  735. // width: 86%;
  736. ::v-deep {
  737. .el-input__wrapper {
  738. background: none;
  739. box-shadow: none;
  740. }
  741. .el-input__inner {
  742. font-size: 14px;
  743. color: rgba(0, 0, 0, 0.5);
  744. }
  745. .el-select__wrapper {
  746. background: none;
  747. box-shadow: none;
  748. gap: 2px;
  749. padding: 4px 2px;
  750. justify-content: center;
  751. }
  752. .el-select__selection {
  753. flex: none;
  754. width: fit-content;
  755. }
  756. .el-select__placeholder {
  757. color: #000;
  758. position: static;
  759. transform: none;
  760. width: fit-content;
  761. }
  762. }
  763. // .select-item {
  764. // width: fit-content;
  765. // }
  766. .period-select {
  767. margin-left: 6px;
  768. }
  769. .select-item {
  770. min-width: 76px;
  771. }
  772. }
  773. ::v-deep {
  774. .el-form-item__label {
  775. color: #000;
  776. }
  777. .el-form-item__error {
  778. top: 117%;
  779. }
  780. .el-form-item {
  781. position: relative;
  782. &::after {
  783. content: "";
  784. position: absolute;
  785. left: 60px;
  786. bottom: -5px;
  787. width: calc(100% - 60px);
  788. height: 1px;
  789. background: rgba(0, 0, 0, 0.08);
  790. }
  791. }
  792. .el-input__wrapper {
  793. box-shadow: none;
  794. padding: 1px 6px;
  795. // border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  796. }
  797. }
  798. .area-box {
  799. display: flex;
  800. align-items: center;
  801. width: 100%;
  802. .unit {
  803. padding-right: 10px;
  804. }
  805. }
  806. .position-wrap {
  807. display: flex;
  808. justify-content: space-between;
  809. align-items: center;
  810. .draw-btn {
  811. flex: none;
  812. padding: 0 12px;
  813. height: 30px;
  814. line-height: 30px;
  815. box-sizing: border-box;
  816. color: #2199f8;
  817. border: 1px solid #2199f8;
  818. background: rgba(33, 153, 248, 0.1);
  819. border-radius: 20px;
  820. font-size: 12px;
  821. }
  822. }
  823. }
  824. .create-btn {
  825. display: flex;
  826. align-items: center;
  827. width: 100%;
  828. padding-top: 10px;
  829. .btn-item {
  830. flex: 1;
  831. text-align: center;
  832. padding: 0 11px;
  833. height: 40px;
  834. line-height: 40px;
  835. border-radius: 34px;
  836. font-size: 16px;
  837. box-sizing: border-box;
  838. &.sencond-btn {
  839. border: 1px solid rgba(153, 153, 153, 0.5);
  840. color: #666666;
  841. }
  842. &.primary-btn {
  843. background: linear-gradient(180deg, #76c3ff, #2199f8);
  844. color: #fff;
  845. }
  846. }
  847. .btn-item + .btn-item {
  848. margin-left: 5px;
  849. }
  850. }
  851. }
  852. }
  853. }
  854. </style>
  855. <style lang="scss">
  856. .location-search-popper {
  857. .el-select-dropdown__list {
  858. max-width: 96vw;
  859. overflow-x: auto;
  860. }
  861. .sub-title {
  862. padding-left: 6px;
  863. font-size: 12px;
  864. color: #ccc;
  865. }
  866. }
  867. </style>