selectVariety.vue 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119
  1. <template>
  2. <div class="select-variety">
  3. <div class="select-variety__content">
  4. <div class="page-header">
  5. <div class="page-title">完善种植品种</div>
  6. <div class="page-title">精准管理每一块地块<span class="page-title-tip">(单选)</span></div>
  7. </div>
  8. <div class="variety-card" v-loading="varietyLoading">
  9. <!-- 单品类标题 -->
  10. <div class="category-title" v-if="currentCategory?.name">
  11. <span class="category-title__line"></span>
  12. <span class="category-title__diamond"></span>
  13. <span class="category-title__text">品类 - {{ currentCategory.name }}</span>
  14. <span class="category-title__diamond"></span>
  15. <span class="category-title__line right-line"></span>
  16. </div>
  17. <el-input v-model="searchKeyword" class="search-bar" placeholder="请输入品种" @keyup.enter="handleSearch">
  18. <template #prefix>
  19. <el-icon class="search-icon">
  20. <Search />
  21. </el-icon>
  22. </template>
  23. <template #suffix>
  24. <span class="search-btn" @click="handleSearch">搜索</span>
  25. </template>
  26. </el-input>
  27. <div class="variety-grid">
  28. <div v-for="item in visibleVarieties" :key="item.id" class="variety-item"
  29. :class="{ selected: isSelected(item.id) }" @click="toggleVariety(item)">
  30. {{ item.name }}
  31. </div>
  32. </div>
  33. <div v-if="!varietyLoading && !filteredVarieties.length" class="empty-tip">暂无品种</div>
  34. <div v-if="filteredVarieties.length > DEFAULT_VISIBLE_COUNT" class="expand-trigger"
  35. @click="gridExpanded = !gridExpanded">
  36. {{ gridExpanded ? "点击收起" : "点击展开更多" }}
  37. </div>
  38. </div>
  39. <!-- 默认表单卡(单选,始终一张) -->
  40. <div v-if="currentSelected" :key="currentSelected.uid" class="form-card"
  41. :class="hasVariety ? 'selected-card' : 'form-card--default'">
  42. <div class="form-card__header">
  43. <img class="title-icon" src="@/assets/img/home/label-icon.png" alt="" />
  44. <el-select
  45. v-model="currentSelected.id"
  46. class="variety-select"
  47. placeholder="选择品种"
  48. popper-class="variety-select-popper"
  49. @change="handleFormVarietyChange"
  50. >
  51. <el-option
  52. v-for="opt in currentVarieties"
  53. :key="opt.id"
  54. :label="opt.name"
  55. :value="opt.id"
  56. />
  57. </el-select>
  58. <!-- <span v-if="hasVariety" class="delete-btn" @click="clearVariety">删除</span> -->
  59. </div>
  60. <div
  61. class="form-card__body"
  62. :class="{ 'form-card__body--locked': !hasVariety }"
  63. @click="onFormBodyClick"
  64. >
  65. <div class="form-row">
  66. <span class="form-label">当下物候期</span>
  67. <el-select
  68. v-model="currentSelected.phenologyId"
  69. class="form-select"
  70. placeholder="选择物候期"
  71. placement="bottom-end"
  72. popper-class="variety-select-popper"
  73. >
  74. <el-option
  75. v-for="opt in currentPhenologyOptions"
  76. :key="opt.id"
  77. :label="opt.name"
  78. :value="opt.id"
  79. />
  80. </el-select>
  81. </div>
  82. <div class="form-row">
  83. <span class="form-label" :class="{ muted: !hasVariety }">
  84. {{ currentStartingPointQuestion }}
  85. </span>
  86. <el-date-picker
  87. v-model="currentSelected.startTime"
  88. class="form-picker"
  89. type="date"
  90. placeholder="请选择时间"
  91. format="YYYY-MM-DD"
  92. value-format="YYYY-MM-DD"
  93. :clearable="false"
  94. style="width: 130px"
  95. :readonly="!hasVariety"
  96. />
  97. </div>
  98. <div class="form-row">
  99. <span class="form-label">种植亩数</span>
  100. <el-input
  101. v-model="currentSelected.area"
  102. class="form-input"
  103. placeholder="请输入亩数"
  104. type="number"
  105. :readonly="!hasVariety"
  106. />
  107. </div>
  108. <div class="form-row location-row">
  109. <span class="form-label">选择种植点位</span>
  110. <div
  111. class="map"
  112. :class="{ 'map--clickable': hasVariety }"
  113. :ref="(el) => setItemMapRef(currentSelected.uid, el)"
  114. @click="goSelectItemLocation(currentSelected)"
  115. ></div>
  116. </div>
  117. </div>
  118. </div>
  119. </div>
  120. <div class="custom-bottom-fixed-btns">
  121. <div class="bottom-btn secondary-btn" @click="handlePrev">上一步</div>
  122. <div class="bottom-btn primary-btn" @click="handleConfirm">{{ varietyNextText }}</div>
  123. </div>
  124. </div>
  125. </template>
  126. <script setup>
  127. import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from "vue";
  128. import { ElMessage } from "element-plus";
  129. import { Search } from "@element-plus/icons-vue";
  130. import { useRouter } from "vue-router";
  131. import { useStore } from "vuex";
  132. import SelectLocationMap, { POINT_ICON } from "../map/selectLocationMap.js";
  133. const emit = defineEmits(["prev", "next"]);
  134. const props = defineProps({
  135. multiple: { type: Boolean, default: false },
  136. isLastStep: { type: Boolean, default: false },
  137. });
  138. const router = useRouter();
  139. const store = useStore();
  140. const varietyNextText = computed(() => (props.isLastStep ? "提交信息" : "下一步 (3/4)"));
  141. const DEFAULT_VISIBLE_COUNT = 12;
  142. const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
  143. const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
  144. const EDIT_UID_KEY = "ENTRY_VARIETY_EDIT_UID";
  145. const SELECTED_LIST_KEY = "ENTRY_SELECTED_VARIETY_LIST";
  146. const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
  147. const MAP_PADDING = [6, 6, 6, 6];
  148. let selectedUid = 0;
  149. const itemPreviewMaps = new Map();
  150. function getDefaultPoint() {
  151. return (
  152. localStorage.getItem("MINI_USER_LOCATION_POINT") ||
  153. store.state.home.miniUserLocationPoint ||
  154. DEFAULT_POINT
  155. );
  156. }
  157. function utilWktToCoordinate(point) {
  158. const match = String(point).match(/POINT\s*\(([\d.\-]+)\s+([\d.\-]+)\)/i);
  159. if (!match) return null;
  160. return [Number(match[1]), Number(match[2])];
  161. }
  162. function createPreviewMap(el, point, showPoint = false) {
  163. if (!el || !point) return null;
  164. const map = new SelectLocationMap({ iconSrc: POINT_ICON.plant });
  165. map.initMap(point, el, {
  166. enableClick: false,
  167. padding: MAP_PADDING,
  168. showPoint,
  169. });
  170. nextTick(() => {
  171. map.kmap?.map?.updateSize?.();
  172. });
  173. return map;
  174. }
  175. function destroyPreviewMap(map) {
  176. map?.clearLayer();
  177. }
  178. function updateItemMap(uid, point, showPoint) {
  179. const map = itemPreviewMaps.get(uid);
  180. if (!map?.kmap || !point) return;
  181. const coordinate = utilWktToCoordinate(point);
  182. if (!coordinate) return;
  183. if (showPoint) {
  184. map.showPoint(coordinate);
  185. } else {
  186. map.hidePoint();
  187. map.setMapPosition(coordinate, false);
  188. }
  189. }
  190. function setItemMapRef(uid, el) {
  191. if (!el) {
  192. destroyPreviewMap(itemPreviewMaps.get(uid));
  193. itemPreviewMaps.delete(uid);
  194. return;
  195. }
  196. if (itemPreviewMaps.has(uid)) {
  197. nextTick(() => itemPreviewMaps.get(uid)?.kmap?.map?.updateSize?.());
  198. return;
  199. }
  200. nextTick(() => {
  201. const item = selectedList.value.find((i) => i.uid === uid);
  202. const point = item?.location || getDefaultPoint();
  203. const map = createPreviewMap(el, point, !!item?.location);
  204. if (map) itemPreviewMaps.set(uid, map);
  205. });
  206. }
  207. function syncItemLocationFromSession() {
  208. const uid = sessionStorage.getItem(EDIT_UID_KEY);
  209. if (!uid) return;
  210. sessionStorage.removeItem(EDIT_UID_KEY);
  211. try {
  212. const raw = sessionStorage.getItem(LOCATION_KEY);
  213. const saved = raw ? JSON.parse(raw) : null;
  214. if (!saved?.point) return;
  215. const item = selectedList.value.find((i) => i.uid === uid);
  216. if (!item) return;
  217. item.location = saved.point;
  218. saveSelectedListDraft();
  219. nextTick(() => updateItemMap(uid, saved.point, true));
  220. } catch {
  221. // ignore
  222. }
  223. }
  224. function saveSelectedListDraft() {
  225. try {
  226. sessionStorage.setItem(
  227. SELECTED_LIST_KEY,
  228. JSON.stringify({
  229. selectedUid,
  230. activeCategoryId: activeCategoryId.value,
  231. list: selectedList.value,
  232. })
  233. );
  234. } catch {
  235. // ignore
  236. }
  237. }
  238. function restoreSelectedListDraft() {
  239. try {
  240. const raw = sessionStorage.getItem(SELECTED_LIST_KEY);
  241. if (!raw) return;
  242. const draft = JSON.parse(raw);
  243. if (draft?.activeCategoryId != null) {
  244. activeCategoryId.value = draft.activeCategoryId;
  245. }
  246. if (typeof draft?.selectedUid === "number") {
  247. selectedUid = draft.selectedUid;
  248. }
  249. if (Array.isArray(draft?.list) && draft.list.length) {
  250. selectedList.value = props.multiple ? draft.list : draft.list.slice(0, 1);
  251. }
  252. } catch {
  253. // ignore
  254. }
  255. }
  256. const goSelectItemLocation = (item) => {
  257. if (!item?.id) {
  258. ElMessage.warning("请选择品种");
  259. return;
  260. }
  261. sessionStorage.setItem(EDIT_UID_KEY, item.uid);
  262. sessionStorage.setItem("ENTRY_INFORMATION_STEP", "3");
  263. saveSelectedListDraft();
  264. router.push({
  265. path: "/entry_select_location",
  266. query: {
  267. mapCenter: item.location || getDefaultPoint(),
  268. pointType: "plant",
  269. },
  270. });
  271. };
  272. onMounted(async () => {
  273. initCategoryTabsFromSession();
  274. restoreSelectedListDraft();
  275. ensureDefaultForm();
  276. syncItemLocationFromSession();
  277. await Promise.all(
  278. categoryTabs.value.map((tab) => fetchVarietiesByCrop(tab.id))
  279. );
  280. });
  281. onActivated(async () => {
  282. initCategoryTabsFromSession();
  283. restoreSelectedListDraft();
  284. ensureDefaultForm();
  285. syncItemLocationFromSession();
  286. nextTick(() => {
  287. itemPreviewMaps.forEach((map) => map.kmap?.map?.updateSize?.());
  288. });
  289. await Promise.all(
  290. categoryTabs.value.map((tab) => fetchVarietiesByCrop(tab.id))
  291. );
  292. });
  293. onBeforeUnmount(() => {
  294. itemPreviewMaps.forEach((map) => destroyPreviewMap(map));
  295. itemPreviewMaps.clear();
  296. });
  297. const getSelectedCategoriesFromSession = () => {
  298. try {
  299. const raw = sessionStorage.getItem(CATEGORY_SESSION_KEY);
  300. if (!raw) return [];
  301. const list = JSON.parse(raw);
  302. if (!Array.isArray(list)) return [];
  303. if (!props.multiple) return list;
  304. return [...list].sort((a, b) => {
  305. const rank = (item) => {
  306. if (item?.firstCrop === "大田" || item?.majorKey === "field") return 0;
  307. if (item?.firstCrop === "果树" || item?.majorKey === "fruit") return 1;
  308. return 2;
  309. };
  310. return rank(a) - rank(b);
  311. });
  312. } catch {
  313. return [];
  314. }
  315. };
  316. const mapPhenologyList = (list) => {
  317. if (!Array.isArray(list)) return [];
  318. return list
  319. .map((item, index) => {
  320. if (typeof item === "string") {
  321. return { id: item, name: item };
  322. }
  323. const name = item?.name || item?.phenology || String(item?.id ?? index);
  324. const id = item?.id ?? name;
  325. return { id, name };
  326. })
  327. .filter((item) => item.name);
  328. };
  329. const initCategoryTabsFromSession = () => {
  330. const selected = getSelectedCategoriesFromSession();
  331. const prevLoaded = new Map(
  332. categoryTabs.value.map((tab) => [String(tab.id), tab])
  333. );
  334. categoryTabs.value = selected.map((item) => {
  335. const id = item.id;
  336. const cached = prevLoaded.get(String(id));
  337. if (cached?.loaded) {
  338. return {
  339. ...cached,
  340. name: item.name || cached.name,
  341. code: item.code || cached.code || "",
  342. firstCrop: item.firstCrop || cached.firstCrop || "",
  343. };
  344. }
  345. return {
  346. id,
  347. name: item.name || "",
  348. code: item.code || "",
  349. firstCrop: item.firstCrop || "",
  350. varieties: [],
  351. phenology: [],
  352. startingPointQuestion: "起种点问题",
  353. loaded: false,
  354. };
  355. });
  356. if (!categoryTabs.value.length) {
  357. activeCategoryId.value = null;
  358. return;
  359. }
  360. const stillExists = categoryTabs.value.some(
  361. (tab) => String(tab.id) === String(activeCategoryId.value)
  362. );
  363. if (!stillExists) {
  364. activeCategoryId.value = categoryTabs.value[0].id;
  365. }
  366. };
  367. const categoryTabs = ref([]);
  368. const activeCategoryId = ref(null);
  369. const varietyLoading = ref(false);
  370. const searchKeyword = ref("");
  371. const appliedKeyword = ref("");
  372. const gridExpanded = ref(false);
  373. const selectedList = ref([]);
  374. const fetchVarietiesByCrop = async (cropId, force = false) => {
  375. if (cropId == null || cropId === "") return;
  376. const tab = categoryTabs.value.find((item) => String(item.id) === String(cropId));
  377. if (!tab) return;
  378. if (tab.loaded && !force) return;
  379. const showLoading = String(cropId) === String(activeCategoryId.value);
  380. if (showLoading) varietyLoading.value = true;
  381. try {
  382. const res = await VE_API.entry.getVarietiesByCrop({
  383. crop_id: cropId,
  384. });
  385. if (res.code === 200 && res.data) {
  386. const data = res.data;
  387. tab.varieties = (data.varieties || [])
  388. .map((item) => {
  389. if (typeof item === "string") {
  390. return { id: item, name: item };
  391. }
  392. const name = item?.name || "";
  393. return {
  394. id: item?.id ?? name,
  395. name,
  396. };
  397. })
  398. .filter((item) => item.name);
  399. tab.phenology = mapPhenologyList(data.phenology);
  400. tab.startingPointQuestion =
  401. data.starting_point_question || "起种点问题";
  402. tab.loaded = true;
  403. } else {
  404. tab.varieties = [];
  405. tab.phenology = [];
  406. tab.startingPointQuestion = "起种点问题";
  407. if (showLoading) {
  408. ElMessage.error(res.msg || "获取品种列表失败");
  409. }
  410. }
  411. } catch {
  412. tab.varieties = [];
  413. tab.phenology = [];
  414. tab.startingPointQuestion = "起种点问题";
  415. if (showLoading) {
  416. ElMessage.error("获取品种列表失败,请稍后再试");
  417. }
  418. } finally {
  419. if (showLoading) varietyLoading.value = false;
  420. }
  421. };
  422. watch(
  423. selectedList,
  424. () => {
  425. saveSelectedListDraft();
  426. },
  427. { deep: true }
  428. );
  429. const currentCategory = computed(
  430. () =>
  431. categoryTabs.value.find((tab) => String(tab.id) === String(activeCategoryId.value)) ||
  432. categoryTabs.value[0]
  433. );
  434. const getPhenologyOptions = (categoryId) => {
  435. const tab = categoryTabs.value.find((item) => String(item.id) === String(categoryId));
  436. return tab?.phenology || [];
  437. };
  438. const getStartingPointQuestion = (categoryId) => {
  439. const tab = categoryTabs.value.find((item) => String(item.id) === String(categoryId));
  440. return tab?.startingPointQuestion || "起种点问题";
  441. };
  442. const currentVarieties = computed(() => currentCategory.value?.varieties || []);
  443. const getVarietiesByCategory = (categoryId) => {
  444. const tab = categoryTabs.value.find((item) => String(item.id) === String(categoryId));
  445. return tab?.varieties || [];
  446. };
  447. const filteredVarieties = computed(() => {
  448. const list = currentVarieties.value;
  449. const kw = appliedKeyword.value.trim();
  450. if (!kw) return list;
  451. return list.filter((item) => item.name.includes(kw));
  452. });
  453. const visibleVarieties = computed(() => {
  454. if (gridExpanded.value) return filteredVarieties.value;
  455. return filteredVarieties.value.slice(0, DEFAULT_VISIBLE_COUNT);
  456. });
  457. const createEmptyForm = (category) => ({
  458. uid: `sel-${++selectedUid}`,
  459. id: "",
  460. name: "",
  461. categoryId: category?.id ?? null,
  462. categoryName: category?.name ?? "",
  463. cropCode: category?.code || "",
  464. firstCrop: category?.firstCrop || "",
  465. phenologyId: "",
  466. startTime: "",
  467. area: "",
  468. location: "",
  469. });
  470. /** 始终保留可编辑表单卡:农户一张;农服按品类各一张 */
  471. const ensureDefaultForm = () => {
  472. const category = currentCategory.value;
  473. if (!category) {
  474. if (!props.multiple) selectedList.value = [];
  475. return;
  476. }
  477. if (props.multiple) {
  478. const categoryIds = new Set(categoryTabs.value.map((tab) => String(tab.id)));
  479. selectedList.value = selectedList.value.filter((item) =>
  480. categoryIds.has(String(item.categoryId))
  481. );
  482. const exists = selectedList.value.some(
  483. (item) => String(item.categoryId) === String(category.id)
  484. );
  485. if (!exists) {
  486. selectedList.value.push(createEmptyForm(category));
  487. }
  488. return;
  489. }
  490. if (!selectedList.value.length) {
  491. selectedList.value = [createEmptyForm(category)];
  492. return;
  493. }
  494. const item = selectedList.value[0];
  495. if (String(item.categoryId) !== String(category.id)) {
  496. selectedList.value = [createEmptyForm(category)];
  497. return;
  498. }
  499. item.categoryId = category.id;
  500. item.categoryName = category.name;
  501. item.cropCode = category.code || item.cropCode || "";
  502. item.firstCrop = category.firstCrop || item.firstCrop || "";
  503. };
  504. const getCurrentForm = () => {
  505. if (props.multiple) {
  506. return (
  507. selectedList.value.find(
  508. (item) => String(item.categoryId) === String(activeCategoryId.value)
  509. ) || null
  510. );
  511. }
  512. return selectedList.value[0] || null;
  513. };
  514. const currentSelected = computed(() => getCurrentForm());
  515. const hasVariety = computed(() => !!currentSelected.value?.id);
  516. const currentPhenologyOptions = computed(() =>
  517. getPhenologyOptions(currentSelected.value?.categoryId || activeCategoryId.value)
  518. );
  519. const currentStartingPointQuestion = computed(() =>
  520. getStartingPointQuestion(currentSelected.value?.categoryId || activeCategoryId.value)
  521. );
  522. watch(activeCategoryId, (id) => {
  523. searchKeyword.value = "";
  524. appliedKeyword.value = "";
  525. gridExpanded.value = false;
  526. ensureDefaultForm();
  527. if (id != null) {
  528. fetchVarietiesByCrop(id);
  529. }
  530. });
  531. const isSelected = (id) =>
  532. !!id && String(getCurrentForm()?.id) === String(id);
  533. const handleSearch = () => {
  534. appliedKeyword.value = searchKeyword.value;
  535. gridExpanded.value = false;
  536. };
  537. const applyVarietyToForm = (variety) => {
  538. ensureDefaultForm();
  539. const item = getCurrentForm();
  540. if (!item || !variety || !currentCategory.value) return;
  541. item.id = variety.id;
  542. item.name = variety.name;
  543. item.categoryId = currentCategory.value.id;
  544. item.categoryName = currentCategory.value.name;
  545. item.cropCode = currentCategory.value.code || item.cropCode || "";
  546. item.firstCrop = currentCategory.value.firstCrop || item.firstCrop || "";
  547. item.phenologyId = "";
  548. saveSelectedListDraft();
  549. };
  550. const handleFormVarietyChange = (varietyId) => {
  551. if (!varietyId) {
  552. clearVariety();
  553. return;
  554. }
  555. const found = currentVarieties.value.find((item) => String(item.id) === String(varietyId));
  556. if (!found) return;
  557. applyVarietyToForm(found);
  558. };
  559. const clearVariety = () => {
  560. ensureDefaultForm();
  561. const item = getCurrentForm();
  562. if (!item) return;
  563. item.id = "";
  564. item.name = "";
  565. item.phenologyId = "";
  566. item.startTime = "";
  567. item.area = "";
  568. item.location = "";
  569. nextTick(() => updateItemMap(item.uid, getDefaultPoint(), false));
  570. saveSelectedListDraft();
  571. };
  572. const onFormBodyClick = () => {
  573. if (!hasVariety.value) {
  574. ElMessage.warning("请选择品种");
  575. }
  576. };
  577. const toggleVariety = (variety) => {
  578. const current = getCurrentForm();
  579. if (current && String(current.id) === String(variety.id)) {
  580. clearVariety();
  581. return;
  582. }
  583. applyVarietyToForm(variety);
  584. };
  585. const handlePrev = () => {
  586. const idx = categoryTabs.value.findIndex(
  587. (tab) => String(tab.id) === String(activeCategoryId.value)
  588. );
  589. if (idx > 0) {
  590. activeCategoryId.value = categoryTabs.value[idx - 1].id;
  591. saveSelectedListDraft();
  592. return;
  593. }
  594. emit("prev");
  595. };
  596. const getBaForm = () => {
  597. try {
  598. const raw = sessionStorage.getItem("ENTRY_BA_FORM");
  599. return raw ? JSON.parse(raw) : null;
  600. } catch {
  601. return null;
  602. }
  603. };
  604. const validateItem = (item, categoryName = "") => {
  605. const label = item?.name || categoryName;
  606. if (!item?.id) {
  607. ElMessage.warning(categoryName ? `请选择「${categoryName}」的品种` : "请选择一个品种");
  608. return false;
  609. }
  610. if (!item.phenologyId) {
  611. ElMessage.warning(`请选择「${label}」的当下物候期`);
  612. return false;
  613. }
  614. if (!item.startTime) {
  615. ElMessage.warning(`请填写「${label}」的起种点时间`);
  616. return false;
  617. }
  618. if (item.area === "" || item.area == null || Number(item.area) <= 0) {
  619. ElMessage.warning(`请填写「${label}」的种植亩数`);
  620. return false;
  621. }
  622. if (!item.location) {
  623. ElMessage.warning(`请选择「${label}」的种植点位`);
  624. return false;
  625. }
  626. return true;
  627. };
  628. const validateSubmitData = () => {
  629. if (!categoryTabs.value.length) {
  630. ElMessage.warning("请先选择种植品类");
  631. return false;
  632. }
  633. if (props.multiple) {
  634. for (const category of categoryTabs.value) {
  635. const item = selectedList.value.find(
  636. (row) => String(row.categoryId) === String(category.id)
  637. );
  638. if (!validateItem(item, category.name)) return false;
  639. }
  640. } else if (!validateItem(selectedList.value[0])) {
  641. return false;
  642. }
  643. const baForm = getBaForm();
  644. if (!baForm?.name || !baForm?.phone) {
  645. ElMessage.warning("请先完善个人信息");
  646. return false;
  647. }
  648. return true;
  649. };
  650. const handleConfirm = () => {
  651. const currentCategoryName = currentCategory.value?.name || "";
  652. if (props.multiple && !validateItem(getCurrentForm(), currentCategoryName)) return;
  653. const idx = categoryTabs.value.findIndex(
  654. (tab) => String(tab.id) === String(activeCategoryId.value)
  655. );
  656. if (idx > -1 && idx < categoryTabs.value.length - 1) {
  657. activeCategoryId.value = categoryTabs.value[idx + 1].id;
  658. saveSelectedListDraft();
  659. return;
  660. }
  661. if (!validateSubmitData()) return;
  662. selectedList.value.forEach((row) => {
  663. if (!row.id) return;
  664. const options = getPhenologyOptions(row.categoryId);
  665. const found = options.find((opt) => String(opt.id) === String(row.phenologyId));
  666. row.phenophase = found?.name || String(row.phenologyId || "");
  667. });
  668. saveSelectedListDraft();
  669. emit("next");
  670. };
  671. </script>
  672. <style lang="scss" scoped>
  673. .select-variety {
  674. flex: 1;
  675. min-height: 0;
  676. display: flex;
  677. flex-direction: column;
  678. overflow: hidden;
  679. &__content {
  680. flex: 1;
  681. min-height: 0;
  682. overflow-y: auto;
  683. -webkit-overflow-scrolling: touch;
  684. padding: 20px 10px 100px;
  685. .page-header {
  686. margin-bottom: 20px;
  687. .page-title {
  688. font-size: 26px;
  689. color: #005599;
  690. line-height: 30px;
  691. font-family: "PangMenZhengDao";
  692. .page-title-tip {
  693. font-size: 20px;
  694. }
  695. }
  696. .page-subtitle {
  697. color: rgba(46, 46, 46, 0.4);
  698. }
  699. }
  700. .category-title {
  701. display: flex;
  702. align-items: center;
  703. justify-content: center;
  704. gap: 1px;
  705. height: 24px;
  706. margin-bottom: 9px;
  707. padding: 0 6px;
  708. box-sizing: border-box;
  709. &__line {
  710. flex: 1;
  711. height: 1px;
  712. background: linear-gradient(90deg, rgba(118, 118, 118, 0) 0%, rgba(118, 118, 118, 0.4) 100%);
  713. &.right-line {
  714. background: linear-gradient(270deg, rgba(118, 118, 118, 0) 0%, rgba(118, 118, 118, 0.4) 100%);
  715. }
  716. }
  717. &__diamond {
  718. flex-shrink: 0;
  719. width: 6px;
  720. height: 6px;
  721. background: rgba(61, 61, 61, 0.2);
  722. transform: rotate(45deg);
  723. }
  724. &__text {
  725. flex-shrink: 0;
  726. padding: 0 10px;
  727. font-size: 16px;
  728. color: #3D3D3D;
  729. line-height: 24px;
  730. white-space: nowrap;
  731. }
  732. }
  733. .variety-card {
  734. margin-bottom: 10px;
  735. background: rgba(255, 255, 255, 0.5);
  736. border: 1px solid #fff;
  737. border-radius: 10px;
  738. padding: 10px 6px;
  739. .search-bar {
  740. margin-bottom: 10px;
  741. :deep(.el-input__wrapper) {
  742. height: 36px;
  743. padding: 0 10px;
  744. background: #fff;
  745. border-radius: 6px;
  746. box-shadow: none;
  747. }
  748. :deep(.el-input__inner) {
  749. color: #1a1a1a;
  750. line-height: 20px;
  751. &::placeholder {
  752. color: rgba(29, 33, 41, 0.2);
  753. }
  754. }
  755. .search-icon {
  756. font-size: 16px;
  757. color: rgba(0, 0, 0, 0.2);
  758. }
  759. .search-btn {
  760. color: #2199f8;
  761. }
  762. }
  763. .variety-grid {
  764. display: grid;
  765. grid-template-columns: repeat(4, minmax(0, 1fr));
  766. gap: 8px;
  767. .variety-item {
  768. display: flex;
  769. align-items: center;
  770. justify-content: center;
  771. padding: 7px 0;
  772. background: #fff;
  773. border: 1px solid transparent;
  774. border-radius: 6px;
  775. color: #565656;
  776. text-align: center;
  777. &.selected {
  778. color: #2199f8;
  779. background: rgba(33, 153, 248, 0.1);
  780. border-color: #2199f8;
  781. }
  782. }
  783. }
  784. .expand-trigger {
  785. margin-top: 10px;
  786. text-align: center;
  787. color: rgba(0, 0, 0, 0.6);
  788. }
  789. .empty-tip {
  790. padding: 24px 0 10px;
  791. text-align: center;
  792. color: rgba(0, 0, 0, 0.35);
  793. font-size: 14px;
  794. }
  795. }
  796. .form-card {
  797. margin-bottom: 10px;
  798. background: #fff;
  799. border-radius: 10px;
  800. padding: 12px;
  801. &__header {
  802. display: flex;
  803. align-items: center;
  804. border-bottom: .2px solid rgba(0, 0, 0, 0.05);
  805. padding-bottom: 10px;
  806. .title-icon {
  807. width: 15px;
  808. object-fit: contain;
  809. margin-right: 5px;
  810. }
  811. .variety-select {
  812. flex: 0 0 auto;
  813. width: auto !important;
  814. max-width: calc(100% - 50px);
  815. :deep(.el-select__wrapper) {
  816. box-shadow: none;
  817. padding: 0;
  818. display: inline-flex;
  819. width: auto !important;
  820. }
  821. :deep(.el-select__selection) {
  822. display: inline-flex;
  823. width: auto;
  824. flex: 0 0 auto;
  825. }
  826. :deep(.el-select__placeholder) {
  827. position: static;
  828. font-size: 16px;
  829. color: #000;
  830. transform: none;
  831. }
  832. :deep(.el-select__caret) {
  833. color: #000;
  834. font-size: 16px;
  835. }
  836. }
  837. .delete-btn {
  838. flex-shrink: 0;
  839. margin-left: auto;
  840. padding-left: 12px;
  841. font-size: 14px;
  842. color: #f53f3f;
  843. line-height: 20px;
  844. white-space: nowrap;
  845. }
  846. }
  847. &--default {
  848. .form-card__header {
  849. .variety-select {
  850. :deep(.el-select__selected-item),
  851. :deep(.el-select__placeholder) {
  852. color: #1d2129;
  853. }
  854. :deep(.el-select__placeholder.is-transparent) {
  855. color: rgba(34, 34, 34, 0.3);
  856. }
  857. :deep(.el-select__caret) {
  858. color: rgba(34, 34, 34, 0.3);
  859. }
  860. }
  861. }
  862. .form-card__body--locked {
  863. cursor: pointer;
  864. .form-row {
  865. pointer-events: none;
  866. }
  867. }
  868. }
  869. .form-row {
  870. display: flex;
  871. align-items: center;
  872. justify-content: space-between;
  873. gap: 12px;
  874. margin-top: 10px;
  875. .form-label {
  876. font-size: 14px;
  877. color: #1D2129;
  878. &.muted {
  879. color: rgba(29, 33, 41, 0.2);
  880. }
  881. }
  882. .form-input {
  883. flex: 1;
  884. min-width: 0;
  885. :deep(.el-input__wrapper) {
  886. box-shadow: none;
  887. background: transparent;
  888. padding: 0;
  889. justify-content: flex-end;
  890. }
  891. :deep(.el-input__inner) {
  892. text-align: right;
  893. font-size: 14px;
  894. color: #1a1a1a;
  895. &::placeholder {
  896. color: rgba(0, 0, 0, 0.25);
  897. }
  898. }
  899. }
  900. .form-select {
  901. flex: 0 0 auto;
  902. width: auto;
  903. max-width: 160px;
  904. margin-left: auto;
  905. :deep(.el-select__wrapper) {
  906. box-shadow: none;
  907. background: transparent;
  908. padding: 0;
  909. justify-content: flex-end;
  910. min-height: auto;
  911. width: auto;
  912. }
  913. :deep(.el-select__selection) {
  914. justify-content: flex-end;
  915. flex: 0 1 auto;
  916. }
  917. :deep(.el-select__placeholder),
  918. :deep(.el-select__selected-item) {
  919. text-align: right;
  920. font-size: 14px;
  921. color: #1a1a1a;
  922. }
  923. :deep(.el-select__placeholder) {
  924. color: #1D2129;
  925. position: static;
  926. transform: none;
  927. }
  928. :deep(.el-select__placeholder.is-transparent) {
  929. color: rgba(0, 0, 0, 0.25);
  930. }
  931. :deep(.el-select__caret) {
  932. color: rgba(0, 0, 0, 0.25);
  933. }
  934. }
  935. .map {
  936. width: 98px;
  937. height: 60px;
  938. flex-shrink: 0;
  939. margin-left: auto;
  940. border-radius: 8px;
  941. overflow: hidden;
  942. background: linear-gradient(135deg, #e8eef3 0%, #d4dde6 100%);
  943. pointer-events: none;
  944. &--clickable {
  945. pointer-events: auto;
  946. cursor: pointer;
  947. }
  948. }
  949. }
  950. }
  951. }
  952. .custom-bottom-fixed-btns {
  953. display: flex;
  954. justify-content: space-between;
  955. align-items: baseline;
  956. padding: 10px 12px 0 12px;
  957. height: 80px;
  958. background: #fff;
  959. box-sizing: border-box;
  960. box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.4);
  961. .bottom-btn {
  962. padding: 0 30px;
  963. height: 40px;
  964. line-height: 40px;
  965. font-size: 14px;
  966. border-radius: 25px;
  967. text-align: center;
  968. }
  969. .secondary-btn {
  970. color: #2199f8;
  971. background: #fff;
  972. border: 1px solid #2199f8;
  973. }
  974. .primary-btn {
  975. background: #2199f8;
  976. color: #fff;
  977. &.disabled {
  978. opacity: 0.6;
  979. pointer-events: none;
  980. }
  981. }
  982. }
  983. }
  984. </style>
  985. <style lang="scss">
  986. .variety-select-popper {
  987. width: auto !important;
  988. min-width: 100px !important;
  989. .el-select-dropdown__item.is-variety-selected {
  990. color: #2199f8;
  991. background: transparent;
  992. font-weight: 500;
  993. &.is-hovering,
  994. &:hover {
  995. color: #2199f8;
  996. background: transparent;
  997. }
  998. }
  999. }
  1000. </style>