allGarden.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. <template>
  2. <div class="all-garden-page">
  3. <custom-header :name="t('agriFile.moreFarms')" />
  4. <div class="page-body">
  5. <div class="map-wrap">
  6. <div class="map-container" ref="mapContainer"></div>
  7. <div class="add-farm-btn" @click="handleAddFarm">
  8. <el-icon><Plus /></el-icon>
  9. <span>{{ t("garden.addFarm") }}</span>
  10. </div>
  11. </div>
  12. <div class="farm-filter">
  13. <el-input
  14. v-model="searchFarm"
  15. class="filter-search"
  16. :placeholder="t('weather.searchFarm')"
  17. :prefix-icon="Search"
  18. style="width: 106px"
  19. clearable
  20. />
  21. <div class="filter-select-wrap">
  22. <el-select
  23. v-model="regionVal"
  24. class="filter-select"
  25. :placeholder="t('weather.selectRegion')"
  26. clearable
  27. >
  28. <el-option
  29. v-for="item in regionOptions"
  30. :key="item.value"
  31. :label="item.label"
  32. :value="item.value"
  33. />
  34. </el-select>
  35. <el-select
  36. v-model="typeVal"
  37. class="filter-select"
  38. :placeholder="t('weather.selectCategory')"
  39. clearable
  40. >
  41. <el-option
  42. v-for="item in typeOptions"
  43. :key="item.value"
  44. :label="item.label"
  45. :value="item.value"
  46. />
  47. </el-select>
  48. </div>
  49. </div>
  50. <div class="farm-list">
  51. <div
  52. v-for="item in filteredFarmList"
  53. :key="item.id"
  54. class="farm-card"
  55. @click.stop="handleEnterFarm(item)"
  56. :class="{ 'is-current': item.isCurrent }"
  57. >
  58. <div class="farm-card__header">
  59. <div class="farm-card__main">
  60. <div class="farm-card__title-row">
  61. <span class="farm-name van-ellipsis">{{ item.name }}</span>
  62. <span v-if="item.categoryLabel" class="category-tag">{{ item.categoryLabel }}</span>
  63. </div>
  64. <div class="farm-address van-ellipsis">
  65. {{ item.rawAddress || t("common.noAddress") }}
  66. </div>
  67. </div>
  68. <div
  69. v-if="item.isCurrent"
  70. class="action-btn action-btn-current"
  71. >
  72. {{ t("garden.currentFarm") }}
  73. </div>
  74. <div
  75. v-else
  76. class="action-btn action-btn-enter"
  77. @click.stop="handleEnterFarm(item)"
  78. >
  79. {{ t("garden.enterFarm") }}
  80. </div>
  81. </div>
  82. <div class="phenology-box">
  83. {{ t("garden.currentPhenology") }}{{ item.phenologyText || t("garden.phenologyPlaceholder") }}
  84. </div>
  85. <div class="task-bar">
  86. <div class="task-bar__left">
  87. <img class="task-icon" src="@/assets/img/home/task.png" alt="" />
  88. <span class="task-name van-ellipsis">{{ item.taskName || t("garden.taskNamePlaceholder") }}</span>
  89. <span class="task-status">{{ item.taskStatus || t("garden.pendingExecute") }}</span>
  90. </div>
  91. <div class="task-bar__forward" @click.stop="handleForward(item)">
  92. <Icon size="14" color="#2199F8" name="share" />
  93. <span>{{ t("garden.forward") }}</span>
  94. </div>
  95. </div>
  96. </div>
  97. <div v-if="!filteredFarmList.length" class="empty-tip">
  98. {{ t("garden.noFarm") }}
  99. </div>
  100. </div>
  101. </div>
  102. <div class="invite-bar">
  103. <div class="invite-btn" @click="handleInviteFarmer">
  104. <img class="invite-btn-icon" src="@/assets/img/home/user-icon.png" alt="">
  105. <span>{{ t("garden.inviteFarmer") }}</span>
  106. </div>
  107. <div class="invite-btn" @click="handleInviteService">
  108. <img class="invite-btn-icon" src="@/assets/img/home/user-icon.png" alt="">
  109. <span>{{ t("garden.inviteService") }}</span>
  110. </div>
  111. </div>
  112. </div>
  113. </template>
  114. <script setup>
  115. import { computed, nextTick, onActivated, onMounted, ref, watch } from "vue";
  116. import { useRouter } from "vue-router";
  117. import { useStore } from "vuex";
  118. import { ElMessage } from "element-plus";
  119. import { Plus, Search, Share, User, UserFilled } from "@element-plus/icons-vue";
  120. import { useI18n } from "@/i18n";
  121. import customHeader from "@/components/customHeader.vue";
  122. import IndexMap from "@/views/old_mini/work_execute/index.js";
  123. import { Icon } from 'vant';
  124. const { t } = useI18n();
  125. const router = useRouter();
  126. const store = useStore();
  127. const mapContainer = ref(null);
  128. const indexMap = new IndexMap();
  129. const farmList = ref([]);
  130. const searchFarm = ref("");
  131. const regionVal = ref("");
  132. const typeVal = ref("");
  133. const regionOptions = computed(() => {
  134. const set = new Set();
  135. farmList.value.forEach((farm) => {
  136. const region = farm.city || farm.farm_city || "";
  137. if (region) set.add(region);
  138. });
  139. return Array.from(set).map((value) => ({ label: value, value }));
  140. });
  141. const typeOptions = computed(() => {
  142. const set = new Set();
  143. farmList.value.forEach((farm) => {
  144. if (farm.categoryLabel) set.add(farm.categoryLabel);
  145. });
  146. return Array.from(set).map((value) => ({ label: value, value }));
  147. });
  148. const filteredFarmList = computed(() => {
  149. const keyword = searchFarm.value.trim().toLowerCase();
  150. return farmList.value.filter((farm) => {
  151. if (keyword && !(farm.name || "").toLowerCase().includes(keyword)) {
  152. return false;
  153. }
  154. if (regionVal.value) {
  155. const region = farm.city || farm.farm_city || "";
  156. if (region !== regionVal.value) return false;
  157. }
  158. if (typeVal.value && farm.categoryLabel !== typeVal.value) {
  159. return false;
  160. }
  161. return true;
  162. });
  163. });
  164. function resolveFarmPointWkt(farm) {
  165. if (farm.geom_wkt && /^POINT\s*\(/i.test(String(farm.geom_wkt).trim())) {
  166. return farm.geom_wkt;
  167. }
  168. if (farm.farm_location && /^POINT\s*\(/i.test(String(farm.farm_location).trim())) {
  169. return farm.farm_location;
  170. }
  171. return farm.geom_wkt || "";
  172. }
  173. function getSelectedFarmId() {
  174. const stored = localStorage.getItem("selectedFarmId");
  175. if (stored != null && stored !== "") {
  176. return Number(stored);
  177. }
  178. try {
  179. const farm = JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
  180. const id = farm.farm_id ?? farm.id;
  181. return id != null && id !== "" ? Number(id) : null;
  182. } catch {
  183. return null;
  184. }
  185. }
  186. function normalizeFarmList(data) {
  187. const selectedId = getSelectedFarmId();
  188. return (data || []).map((farm) => {
  189. const id = farm.farm_id;
  190. return {
  191. ...farm,
  192. id,
  193. name: farm.farm_name || "",
  194. wkt: resolveFarmPointWkt(farm),
  195. farmName: farm.farm_name || "",
  196. rawAddress: farm.farm_address || "",
  197. categoryLabel: farm.variety_name || "",
  198. phenologyText: farm.period_name || "",
  199. taskName: farm.fw_name || "",
  200. taskStatus: farm.work_status_name || farm.work_status || "",
  201. isCurrent: selectedId != null && Number(id) === selectedId,
  202. };
  203. });
  204. }
  205. function saveSelectedFarm(farm) {
  206. localStorage.setItem("selectedFarmId", farm.id);
  207. localStorage.setItem("selectedFarmName", farm.name || "");
  208. localStorage.setItem("selectedFarmPoint", farm.wkt || "");
  209. localStorage.setItem("selectedFarmData", JSON.stringify(farm));
  210. }
  211. function getDefaultMapPoint() {
  212. const withPoint = farmList.value.find((farm) => farm.wkt);
  213. return withPoint?.wkt || store.state.home.miniUserLocationPoint || "POINT(113.38 23.18)";
  214. }
  215. function refreshMapMarkers() {
  216. if (!indexMap.kmap) return;
  217. const points = filteredFarmList.value.filter((farm) => farm.wkt);
  218. indexMap.initData(points, "farm_name", "wkt", true);
  219. }
  220. function ensureMap() {
  221. if (!mapContainer.value) return;
  222. if (!indexMap.kmap) {
  223. indexMap.initMap(getDefaultMapPoint(), mapContainer.value, true);
  224. }
  225. refreshMapMarkers();
  226. nextTick(() => {
  227. indexMap.kmap?.map?.updateSize?.();
  228. });
  229. }
  230. function getMiniUserId() {
  231. return store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID");
  232. }
  233. function getFarmList() {
  234. const id = getMiniUserId();
  235. if (!id) {
  236. farmList.value = [];
  237. return;
  238. }
  239. VE_API.questionnaire
  240. .getFarms({ id })
  241. .then((res) => {
  242. const list = Array.isArray(res?.data) ? res.data : [];
  243. farmList.value = normalizeFarmList(list);
  244. nextTick(() => {
  245. ensureMap();
  246. });
  247. })
  248. .catch(() => {
  249. farmList.value = [];
  250. });
  251. }
  252. function handleEnterFarm(item) {
  253. saveSelectedFarm(item);
  254. ElMessage.success(t("garden.enterFarmSuccess"));
  255. router.back();
  256. }
  257. function handleAddFarm() {
  258. router.push("/entry_information");
  259. }
  260. function handleForward() {
  261. ElMessage.info(t("garden.forwardTip"));
  262. }
  263. function handleInviteFarmer() {
  264. ElMessage.info(t("garden.inviteTip"));
  265. }
  266. function handleInviteService() {
  267. ElMessage.info(t("garden.inviteTip"));
  268. }
  269. watch(filteredFarmList, () => {
  270. if (indexMap.kmap) {
  271. refreshMapMarkers();
  272. }
  273. });
  274. onMounted(() => {
  275. getFarmList();
  276. nextTick(() => {
  277. ensureMap();
  278. });
  279. });
  280. onActivated(() => {
  281. getFarmList();
  282. nextTick(() => {
  283. if (!indexMap.kmap) {
  284. ensureMap();
  285. return;
  286. }
  287. if (mapContainer.value && indexMap.kmap.map) {
  288. const checkAndUpdateSize = () => {
  289. const container = mapContainer.value;
  290. if (!container) return;
  291. const rect = container.getBoundingClientRect();
  292. if (rect.width > 0 && rect.height > 0) {
  293. indexMap.kmap.map.updateSize();
  294. refreshMapMarkers();
  295. } else {
  296. setTimeout(checkAndUpdateSize, 100);
  297. }
  298. };
  299. setTimeout(checkAndUpdateSize, 200);
  300. }
  301. });
  302. });
  303. </script>
  304. <style lang="scss" scoped>
  305. .all-garden-page {
  306. display: flex;
  307. flex-direction: column;
  308. height: 100vh;
  309. background: #f5f5f5;
  310. box-sizing: border-box;
  311. }
  312. .page-body {
  313. flex: 1;
  314. overflow: auto;
  315. padding: 10px 12px 108px;
  316. box-sizing: border-box;
  317. }
  318. .map-wrap {
  319. position: relative;
  320. border-radius: 8px;
  321. overflow: hidden;
  322. background: #e8e8e8;
  323. .map-container {
  324. width: 100%;
  325. height: 162px;
  326. clip-path: inset(0 round 8px);
  327. }
  328. .add-farm-btn {
  329. position: absolute;
  330. right: 10px;
  331. bottom: 10px;
  332. z-index: 2;
  333. display: inline-flex;
  334. align-items: center;
  335. gap: 2px;
  336. height: 28px;
  337. padding: 0 10px;
  338. border-radius: 14px;
  339. background: #fff;
  340. color: #2199f8;
  341. font-size: 12px;
  342. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  343. }
  344. }
  345. .farm-filter {
  346. display: flex;
  347. align-items: center;
  348. justify-content: space-between;
  349. gap: 8px;
  350. margin-top: 10px;
  351. .filter-select-wrap {
  352. display: flex;
  353. align-items: center;
  354. gap: 8px;
  355. }
  356. .filter-search {
  357. // flex: 1;
  358. min-width: 0;
  359. }
  360. .filter-select {
  361. width: 92px;
  362. flex-shrink: 0;
  363. }
  364. :deep(.el-input__wrapper),
  365. :deep(.el-select__wrapper) {
  366. min-height: 28px;
  367. line-height: 28px;
  368. height: 28px;
  369. padding: 0 8px;
  370. box-sizing: border-box;
  371. border-radius: 4px;
  372. box-shadow: 0 0 0 1px rgba(180, 180, 180, 0.4) inset;
  373. }
  374. }
  375. .farm-list {
  376. margin-top: 10px;
  377. }
  378. .farm-card {
  379. margin-bottom: 10px;
  380. padding: 10px 12px;
  381. border-radius: 8px;
  382. background: #fff;
  383. box-sizing: border-box;
  384. &.is-current {
  385. border: 1px solid #2199f8;
  386. .farm-name {
  387. color: #2199f8;
  388. }
  389. }
  390. }
  391. .farm-card__header {
  392. display: flex;
  393. align-items: flex-start;
  394. justify-content: space-between;
  395. gap: 8px;
  396. }
  397. .farm-card__main {
  398. flex: 1;
  399. min-width: 0;
  400. }
  401. .farm-card__title-row {
  402. display: flex;
  403. align-items: center;
  404. gap: 8px;
  405. min-width: 0;
  406. }
  407. .farm-name {
  408. max-width: 180px;
  409. font-size: 14px;
  410. color: #1D2129;
  411. font-weight: 500;
  412. }
  413. .category-tag {
  414. flex-shrink: 0;
  415. height: 20px;
  416. padding: 0 8px;
  417. border-radius: 2px;
  418. background: rgba(33, 153, 248, 0.1);
  419. color: #2199f8;
  420. font-size: 12px;
  421. line-height: 20px;
  422. }
  423. .farm-address {
  424. margin-top: 2px;
  425. font-size: 12px;
  426. color: rgba(32, 32, 32, 0.4);
  427. }
  428. .action-btn {
  429. flex-shrink: 0;
  430. height: 28px;
  431. padding: 0 10px;
  432. border-radius: 4px;
  433. font-size: 12px;
  434. line-height: 28px;
  435. }
  436. .action-btn-current {
  437. color: #fff;
  438. background: #2199f8;
  439. }
  440. .action-btn-enter {
  441. color: #fff;
  442. background: #ff953d;
  443. }
  444. .phenology-box {
  445. margin-top: 8px;
  446. padding: 6px 8px;
  447. border-radius: 4px;
  448. background: rgba(160, 160, 160, 0.1);
  449. font-size: 12px;
  450. color: #767676;
  451. line-height: 18px;
  452. }
  453. .task-bar {
  454. display: flex;
  455. align-items: center;
  456. justify-content: space-between;
  457. gap: 8px;
  458. margin-top: 10px;
  459. padding: 8px;
  460. border-radius: 6px;
  461. background: rgba(33, 153, 248, 0.1);
  462. }
  463. .task-bar__left {
  464. display: flex;
  465. align-items: center;
  466. gap: 6px;
  467. min-width: 0;
  468. flex: 1;
  469. }
  470. .task-icon {
  471. width: 16px;
  472. height: 16px;
  473. flex-shrink: 0;
  474. }
  475. .task-name {
  476. max-width: 120px;
  477. font-weight: 500;
  478. font-size: 14px;
  479. color: #000;
  480. }
  481. .task-status {
  482. flex-shrink: 0;
  483. height: 20px;
  484. padding: 0 6px;
  485. border-radius: 2px;
  486. background: #2199f8;
  487. color: #fff;
  488. font-size: 12px;
  489. line-height: 20px;
  490. }
  491. .task-bar__forward {
  492. display: inline-flex;
  493. align-items: center;
  494. gap: 4px;
  495. flex-shrink: 0;
  496. color: #2199f8;
  497. font-size: 14px;
  498. }
  499. .empty-tip {
  500. padding: 40px 0;
  501. text-align: center;
  502. color: #86909c;
  503. font-size: 13px;
  504. }
  505. .invite-bar {
  506. position: fixed;
  507. left: 0;
  508. right: 0;
  509. bottom: 60px;
  510. z-index: 20;
  511. display: flex;
  512. justify-content: center;
  513. gap: 16px;
  514. pointer-events: none;
  515. }
  516. .invite-btn {
  517. pointer-events: auto;
  518. display: inline-flex;
  519. align-items: center;
  520. justify-content: center;
  521. box-sizing: border-box;
  522. gap: 6px;
  523. min-width: 110px;
  524. height: 40px;
  525. padding: 0 16px;
  526. border-radius: 20px;
  527. background: linear-gradient(180deg, #72C1FF 0%, #2199F8 100%);
  528. color: #fff;
  529. font-size: 14px;
  530. // box-shadow: 0 4px 12px rgba(33, 153, 248, 0.35);
  531. box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.1);
  532. .invite-btn-icon {
  533. width: 16px;
  534. height: 16px;
  535. filter: brightness(0) invert(1);
  536. }
  537. }
  538. </style>