farmInfoPopup.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <template>
  2. <popup
  3. v-model:show="showValue"
  4. round
  5. class="farm-info-popup"
  6. :close-on-click-overlay="true"
  7. teleport="body"
  8. >
  9. <!-- 第一步:农场信息填写 -->
  10. <template v-if="currentStep === 1">
  11. <!-- 标题 -->
  12. <div class="popup-title">完善以下信息,生成智能种植方案</div>
  13. <!-- 表单区域 -->
  14. <el-form ref="formRef" :model="formData" :rules="rules" class="farm-form">
  15. <!-- 农场位置 -->
  16. <el-form-item label="农场位置" prop="address">
  17. <el-input v-model="formData.address" placeholder="请输入农场位置" clearable />
  18. </el-form-item>
  19. <!-- 农场品种 -->
  20. <el-form-item label="农场品种" prop="speciesId">
  21. <el-select
  22. v-model="formData.speciesId"
  23. placeholder="请选择"
  24. @change="handleSpecieChange"
  25. >
  26. <el-option
  27. v-for="item in specieList"
  28. :key="item.id"
  29. :label="item.name"
  30. :value="item.id"
  31. />
  32. </el-select>
  33. </el-form-item>
  34. <!-- 农场品类 -->
  35. <el-form-item label="农场品类" prop="typeId">
  36. <el-select
  37. v-model="formData.typeId"
  38. multiple
  39. collapse-tags
  40. placeholder="请选择"
  41. :disabled="!formData.speciesId"
  42. @change="handleFruitsChange"
  43. >
  44. <el-option
  45. v-for="item in fruitsList"
  46. :key="item.id"
  47. :label="item.name"
  48. :value="item.id"
  49. />
  50. </el-select>
  51. </el-form-item>
  52. <!-- 农场亩数 -->
  53. <el-form-item label="农场亩数" prop="mu">
  54. <el-input v-model="formData.mu" placeholder="请输入" type="number">
  55. <template #suffix>
  56. <span class="unit">亩</span>
  57. </template>
  58. </el-input>
  59. </el-form-item>
  60. <!-- 农场名称 -->
  61. <el-form-item label="农场名称" prop="name">
  62. <el-input
  63. v-model="formData.name"
  64. placeholder="请输入农场名称"
  65. clearable
  66. @input="handleNameInput"
  67. />
  68. </el-form-item>
  69. </el-form>
  70. </template>
  71. <!-- 第二步:物候期起始时间填写 -->
  72. <template v-else>
  73. <!-- 提示文字 -->
  74. <div class="popup-title">为了精准匹配种植方案,完善物候信息</div>
  75. <!-- 物候期表单 -->
  76. <el-form ref="phenologyFormRef" :model="phenologyData" :rules="phenologyRules" class="farm-form">
  77. <!-- 物候期选择器 -->
  78. <el-form-item
  79. label="当下物候期"
  80. prop="phenologyId"
  81. >
  82. <el-select
  83. v-model="phenologyData.phenologyId"
  84. placeholder="选择物候期"
  85. style="width: 100%"
  86. @change="handlePhenologyChange"
  87. >
  88. <el-option
  89. v-for="phenology in phenologyList"
  90. :key="phenology.phenologyId"
  91. :label="phenology.phenologyName"
  92. :value="phenology.phenologyId"
  93. />
  94. </el-select>
  95. </el-form-item>
  96. <!-- 日期选择器 -->
  97. <el-form-item
  98. label="起始时间"
  99. prop="phenologyStartDate"
  100. >
  101. <el-date-picker
  102. v-model="phenologyData.phenologyStartDate"
  103. type="date"
  104. placeholder="选择时间"
  105. format="YYYY-MM-DD"
  106. value-format="YYYY-MM-DD"
  107. style="width: 100%"
  108. />
  109. </el-form-item>
  110. </el-form>
  111. </template>
  112. <!-- 确认按钮 -->
  113. <div class="btn-confirm" @click="handleConfirm">确认信息</div>
  114. </popup>
  115. </template>
  116. <script setup>
  117. import { Popup } from "vant";
  118. import { computed, ref, watch, nextTick ,onMounted} from "vue";
  119. import { ElMessage } from "element-plus";
  120. import { convertPointToArray } from "@/utils/index";
  121. import { useStore } from "vuex";
  122. const store = useStore();
  123. const props = defineProps({
  124. // 控制弹窗显示/隐藏
  125. show: {
  126. type: Boolean,
  127. default: false,
  128. },
  129. // 是否在点击遮罩层后关闭弹窗
  130. closeOnClickOverlay: {
  131. type: Boolean,
  132. default: false,
  133. },
  134. // 初始表单数据
  135. initialData: {
  136. type: Object,
  137. default: () => ({}),
  138. },
  139. expertMiniUserId: {
  140. type: [String, Number],
  141. default: "",
  142. },
  143. });
  144. const city = ref("");
  145. const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
  146. function getLocationName(location) {
  147. const params = {
  148. key: MAP_KEY,
  149. location,
  150. };
  151. VE_API.old_mini_map.location(params).then(({ result }) => {
  152. const add = result.formatted_addresses?.recommend ? result.formatted_addresses.recommend : result.address + "";
  153. formData.value.address = add;
  154. city.value = result.address_component?.city + result.address_component?.district || "";
  155. });
  156. }
  157. onMounted(() => {
  158. const arr = convertPointToArray(store.state.home.miniUserLocationPoint);
  159. getLocationName(`${arr[1]},${arr[0]}`);
  160. getSpecieList();
  161. });
  162. function getSpecieList() {
  163. return VE_API.farm.fetchSpecieList({ point: store.state.home.miniUserLocationPoint }).then(({ data }) => {
  164. specieList.value = data || [];
  165. return data;
  166. });
  167. }
  168. function getFruitsList(parentId) {
  169. VE_API.farm.fruitsTypeItemList({ parentId }).then(({ data }) => {
  170. fruitsList.value = data;
  171. });
  172. }
  173. const emit = defineEmits(["update:show", "confirm"]);
  174. // 处理v-model双向绑定
  175. const showValue = computed({
  176. get: () => props.show,
  177. set: (value) => emit("update:show", value),
  178. });
  179. const formRef = ref(null);
  180. const phenologyFormRef = ref(null);
  181. // 当前步骤:1-填写农场信息,2-填写物候期时间
  182. const currentStep = ref(1);
  183. // 用户是否手动修改过农场名称
  184. const isNameEdited = ref(false);
  185. // 表单数据
  186. const formData = ref({
  187. address: "",
  188. speciesId: "",
  189. typeId: [],
  190. mu: "",
  191. name: "",
  192. });
  193. // 物候期列表
  194. const phenologyList = ref([]);
  195. // 物候期表单数据
  196. const phenologyData = ref({});
  197. const specieList = ref([]);
  198. const fruitsList = ref([]);
  199. // 规范化多选品种 typeId(支持字符串/数组)
  200. const normalizeTypeId = (value) => {
  201. if (Array.isArray(value)) return value;
  202. if (value === null || value === undefined || value === "") return [];
  203. return [value];
  204. };
  205. // 自定义验证器:验证农场品种
  206. const validateVariety = (rule, value, callback) => {
  207. const hasType =
  208. Array.isArray(formData.value.typeId)
  209. ? formData.value.typeId.length > 0
  210. : !!formData.value.typeId;
  211. if (!formData.value.speciesId || !hasType) {
  212. callback(new Error("请选择农场品种"));
  213. } else {
  214. callback();
  215. }
  216. };
  217. // 表单验证规则
  218. const rules = {
  219. address: [{ required: true, message: "请输入农场位置", trigger: "blur" }],
  220. speciesId: [{ required: true, validator: validateVariety, trigger: "change" }],
  221. typeId: [{ required: true, validator: validateVariety, trigger: "change" }],
  222. mu: [
  223. { required: true, message: "请输入农场亩数", trigger: "blur" },
  224. { pattern: /^\d+(\.\d+)?$/, message: "请输入有效的数字", trigger: "blur" },
  225. ],
  226. name: [{ required: true, message: "请输入农场名称", trigger: "blur" }],
  227. };
  228. // 物候期表单验证规则
  229. const phenologyRules = {
  230. phenologyId: [{ required: true, message: "请选择物候期", trigger: "change" }],
  231. phenologyStartDate: [{ required: true, message: "请选择起始时间", trigger: "change" }],
  232. };
  233. // 监听初始数据变化
  234. watch(
  235. () => props.initialData,
  236. (newData) => {
  237. if (newData && Object.keys(newData).length > 0) {
  238. Object.assign(formData.value, newData);
  239. formData.value.typeId = normalizeTypeId(formData.value.typeId);
  240. }
  241. },
  242. { immediate: true, deep: true }
  243. );
  244. // 监听弹窗显示状态,重置表单
  245. watch(
  246. () => props.show,
  247. (newVal) => {
  248. if (newVal) {
  249. // 重置步骤为第一步
  250. currentStep.value = 1;
  251. // 重置物候期列表和表单数据
  252. phenologyList.value = [];
  253. phenologyData.value = {};
  254. // 重置名称编辑状态
  255. isNameEdited.value = false;
  256. // 弹窗打开时,如果有初始数据则使用,否则使用默认值
  257. if (props.initialData && Object.keys(props.initialData).length > 0) {
  258. Object.assign(formData.value, props.initialData);
  259. formData.value.typeId = normalizeTypeId(formData.value.typeId);
  260. } else {
  261. formData.value = {
  262. address: "",
  263. speciesId: "",
  264. typeId: [],
  265. mu: "",
  266. name: "",
  267. };
  268. }
  269. // 清除验证状态
  270. nextTick(() => {
  271. formRef.value?.clearValidate();
  272. phenologyFormRef.value?.clearValidate();
  273. });
  274. }
  275. }
  276. );
  277. // 品种1变化时,重置品种2并触发验证
  278. const handleSpecieChange = (val) => {
  279. formData.value.typeId = [];
  280. const specie = specieList.value.find(item => item.id === val);
  281. // 只有在用户没有手动修改名称时,才自动带出默认名称
  282. if (specie && !isNameEdited.value) {
  283. formData.value.name = city.value + specie.name + "农场";
  284. }
  285. getFruitsList(val);
  286. // 触发品种验证
  287. nextTick(() => {
  288. formRef.value?.validateField("speciesId");
  289. });
  290. };
  291. // 品种2变化时,触发验证
  292. const handleFruitsChange = () => {
  293. nextTick(() => {
  294. // 校验农场品种(包含大类和品种)
  295. formRef.value?.validateField("speciesId");
  296. formRef.value?.validateField("typeId");
  297. });
  298. };
  299. // 农场名称输入时,标记为用户已手动修改
  300. const handleNameInput = () => {
  301. isNameEdited.value = true;
  302. };
  303. // 确认信息
  304. const handleConfirm = async () => {
  305. // 第一步:验证农场信息表单
  306. if (currentStep.value === 1) {
  307. if (!formRef.value) return;
  308. try {
  309. await formRef.value.validate();
  310. // 验证通过,获取物候期数据并切换到第二步
  311. await getCurrentAndNextPhenology();
  312. currentStep.value = 2;
  313. } catch (error) {
  314. console.log("表单验证失败", error);
  315. }
  316. }
  317. // 第二步:验证物候期表单并关闭弹窗
  318. else {
  319. if (!phenologyFormRef.value) return;
  320. try {
  321. await phenologyFormRef.value.validate();
  322. // 验证通过,提交所有数据并关闭弹窗
  323. emit("confirm", {
  324. ...formData.value,
  325. ...phenologyData.value,
  326. wkt: store.state.home.miniUserLocationPoint,
  327. expertMiniUserId: props.expertMiniUserId,
  328. containerId: specieList.value.find(item => item.id === formData.value.speciesId)?.defaultContainerId,
  329. });
  330. emit("update:show", false);
  331. } catch (error) {
  332. console.log("物候期表单验证失败", error);
  333. }
  334. }
  335. };
  336. // 获取当前日期(YYYY-MM-DD格式)
  337. const getTodayDate = () => {
  338. const today = new Date();
  339. const year = today.getFullYear();
  340. const month = String(today.getMonth() + 1).padStart(2, "0");
  341. const day = String(today.getDate()).padStart(2, "0");
  342. return `${year}-${month}-${day}`;
  343. };
  344. // 物候期变化时,更新日期为对应物候期的 startDate
  345. const handlePhenologyChange = (phenologyId) => {
  346. const selectedPhenology = phenologyList.value.find(item => item.phenologyId === phenologyId);
  347. if (selectedPhenology && selectedPhenology.startDate) {
  348. phenologyData.value.phenologyStartDate = selectedPhenology.startDate;
  349. }
  350. };
  351. // 获取当前和下一个物候期
  352. const getCurrentAndNextPhenology = async () => {
  353. try {
  354. const { data } = await VE_API.home.getCurrentAndNextPhenology({
  355. expertMiniUserId: props.expertMiniUserId,
  356. containerId: specieList.value.find(item => item.id === formData.value.speciesId)?.defaultContainerId,
  357. });
  358. if (data && Array.isArray(data)) {
  359. phenologyList.value = data;
  360. // 初始化物候期表单数据,日期使用第一个物候期的 startDate
  361. const firstPhenology = data[0];
  362. phenologyData.value = {
  363. phenologyId: firstPhenology?.phenologyId || "",
  364. phenologyStartDate: firstPhenology?.startDate || getTodayDate(),
  365. };
  366. }
  367. } catch (error) {
  368. console.error("获取物候期数据失败", error);
  369. ElMessage.error("获取物候期数据失败");
  370. }
  371. };
  372. </script>
  373. <style scoped lang="scss">
  374. .farm-info-popup {
  375. width: 100%;
  376. padding: 20px 16px;
  377. border-radius: 12px;
  378. .popup-title {
  379. font-size: 16px;
  380. color: #121212;
  381. }
  382. .farm-form {
  383. margin: 16px 0;
  384. background: rgba(33, 153, 248, 0.05);
  385. padding: 10px;
  386. border-radius: 5px;
  387. border: 1px solid rgba(33, 153, 248, 0.2);
  388. :deep(.el-form-item__label) {
  389. color: #1d2129;
  390. }
  391. .variety-select-wrap {
  392. display: flex;
  393. gap: 10px;
  394. width: 100%;
  395. }
  396. .unit {
  397. color: #666;
  398. font-size: 14px;
  399. padding-right: 8px;
  400. }
  401. }
  402. .btn-confirm {
  403. padding: 10px;
  404. background: #2199f8;
  405. color: #ffffff;
  406. border-radius: 4px;
  407. font-size: 16px;
  408. text-align: center;
  409. }
  410. }
  411. </style>