selectVariety.vue 36 KB

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