farmInfoPopup.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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">{{ $t('完善以下信息,生成智能种植方案') }}</div>
  13. <!-- 表单区域 -->
  14. <el-form ref="formRef" :model="formData" :rules="rules" class="farm-form">
  15. <!-- 农场位置 -->
  16. <el-form-item :label="$t('农场位置')" prop="address">
  17. <el-input v-model="formData.address" :placeholder="$t('请输入农场位置')" clearable />
  18. </el-form-item>
  19. <!-- 农场品种 -->
  20. <el-form-item :label="$t('农场品种')" prop="speciesId">
  21. <el-select
  22. v-model="formData.speciesId"
  23. :placeholder="$t('请选择')"
  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="$t('农场品类')" prop="typeIds">
  36. <el-select
  37. v-model="formData.typeIds"
  38. multiple
  39. collapse-tags
  40. :placeholder="$t('请选择')"
  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="$t('农场亩数')" prop="mu">
  54. <el-input v-model="formData.mu" :placeholder="$t('请输入')" type="number">
  55. <template #suffix>
  56. <span class="unit">{{ $t('亩') }}</span>
  57. </template>
  58. </el-input>
  59. </el-form-item>
  60. <!-- 农场名称 -->
  61. <el-form-item :label="$t('农场名称')" prop="name">
  62. <el-input
  63. v-model="formData.name"
  64. :placeholder="$t('请输入农场名称')"
  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">{{ $t('为了精准匹配种植方案,完善物候信息') }}</div>
  75. <!-- 物候期表单 -->
  76. <el-form ref="phenologyFormRef" :model="phenologyData" label-width="92px" :rules="phenologyRules" class="farm-form">
  77. <!-- 物候期选择器 -->
  78. <el-form-item
  79. :label="$t('当下物候期')"
  80. prop="phenologyId"
  81. >
  82. <el-select
  83. v-model="phenologyData.phenologyId"
  84. :placeholder="$t('选择物候期')"
  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="$t('起始时间')"
  99. prop="phenologyStartDate"
  100. >
  101. <el-date-picker
  102. v-model="phenologyData.phenologyStartDate"
  103. type="date"
  104. :placeholder="$t('选择时间')"
  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">{{ $t('确认信息') }}</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. oldUser: {
  144. type: Boolean,
  145. default: false,
  146. },
  147. });
  148. const city = ref("");
  149. const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
  150. function getLocationName(location) {
  151. const params = {
  152. key: MAP_KEY,
  153. location,
  154. };
  155. VE_API.old_mini_map.location(params).then(({ result }) => {
  156. const add = result.formatted_addresses?.recommend ? result.formatted_addresses.recommend : result.address + "";
  157. formData.value.address = add;
  158. city.value = result.address_component?.city + result.address_component?.district || "";
  159. });
  160. }
  161. onMounted(() => {
  162. const arr = convertPointToArray(store.state.home.miniUserLocationPoint);
  163. getLocationName(`${arr[1]},${arr[0]}`);
  164. getSpecieList();
  165. });
  166. function getSpecieList() {
  167. return VE_API.farm.fetchSpecieList({ point: store.state.home.miniUserLocationPoint }).then(({ data }) => {
  168. specieList.value = data || [];
  169. return data;
  170. });
  171. }
  172. function getFruitsList(parentId) {
  173. VE_API.farm.fruitsTypeItemList({ parentId }).then(({ data }) => {
  174. fruitsList.value = data;
  175. });
  176. }
  177. const emit = defineEmits(["update:show"]);
  178. // 处理v-model双向绑定
  179. const showValue = computed({
  180. get: () => props.show,
  181. set: (value) => emit("update:show", value),
  182. });
  183. const formRef = ref(null);
  184. const phenologyFormRef = ref(null);
  185. // 当前步骤:1-填写农场信息,2-填写物候期时间
  186. const currentStep = ref(1);
  187. // 用户是否手动修改过农场名称
  188. const isNameEdited = ref(false);
  189. // 表单数据
  190. const formData = ref({
  191. address: "",
  192. speciesId: "",
  193. typeIds: [],
  194. mu: "",
  195. name: "",
  196. });
  197. // 物候期列表
  198. const phenologyList = ref([]);
  199. // 物候期表单数据
  200. const phenologyData = ref({});
  201. const specieList = ref([]);
  202. const fruitsList = ref([]);
  203. // 规范化多选品种 typeId(支持字符串/数组),确保最终一定是数组
  204. const normalizeTypeId = (value) => {
  205. if (Array.isArray(value)) return value;
  206. if (value === null || value === undefined || value === "") return [];
  207. return [value];
  208. };
  209. // 自定义验证器:验证农场品种(只校验 speciesId)
  210. const validateSpeciesId = (rule, value, callback) => {
  211. if (!value) {
  212. callback(new Error("请选择农场品种"));
  213. } else {
  214. callback();
  215. }
  216. };
  217. // 自定义验证器:验证农场品类(只校验 typeIds)
  218. const validateTypeIds = (rule, value, callback) => {
  219. const hasType =
  220. Array.isArray(value)
  221. ? value.length > 0
  222. : !!value;
  223. if (!hasType) {
  224. callback(new Error("请选择农场品类"));
  225. } else {
  226. callback();
  227. }
  228. };
  229. // 表单验证规则
  230. const rules = {
  231. address: [{ required: true, message: "请输入农场位置", trigger: "blur" }],
  232. speciesId: [{ required: true, validator: validateSpeciesId, trigger: "change" }],
  233. typeIds: [{ required: true, validator: validateTypeIds, trigger: "change" }],
  234. mu: [
  235. { required: true, message: "请输入农场亩数", trigger: "blur" },
  236. { pattern: /^\d+(\.\d+)?$/, message: "请输入有效的数字", trigger: "blur" },
  237. ],
  238. name: [{ required: true, message: "请输入农场名称", trigger: "blur" }],
  239. };
  240. // 物候期表单验证规则
  241. const phenologyRules = {
  242. phenologyId: [{ required: true, message: "请选择物候期", trigger: "change" }],
  243. phenologyStartDate: [{ required: true, message: "请选择起始时间", trigger: "change" }],
  244. };
  245. // 监听初始数据变化
  246. watch(
  247. () => props.initialData,
  248. (newData) => {
  249. if (newData && Object.keys(newData).length > 0) {
  250. Object.assign(formData.value, newData);
  251. formData.value.typeIds = normalizeTypeId(formData.value.typeIds);
  252. }
  253. },
  254. { immediate: true, deep: true }
  255. );
  256. // 无论选择几个品类,始终保证 formData.typeIds 为数组
  257. watch(
  258. () => formData.value.typeIds,
  259. (newVal) => {
  260. // 仅在不是数组时进行规范化,避免死循环
  261. if (!Array.isArray(newVal)) {
  262. formData.value.typeIds = normalizeTypeId(newVal);
  263. }
  264. }
  265. );
  266. // 监听弹窗显示状态,重置表单
  267. watch(
  268. () => props.show,
  269. (newVal) => {
  270. if (newVal) {
  271. // 重置物候期列表和表单数据
  272. phenologyList.value = [];
  273. phenologyData.value = {};
  274. // 重置名称编辑状态
  275. isNameEdited.value = false;
  276. // 弹窗打开时,如果有初始数据则使用,否则使用默认值
  277. if (props.initialData && Object.keys(props.initialData).length > 0) {
  278. Object.assign(formData.value, props.initialData);
  279. formData.value.typeIds = normalizeTypeId(formData.value.typeIds);
  280. } else {
  281. formData.value = {
  282. address: "",
  283. speciesId: "",
  284. typeIds: [],
  285. mu: "",
  286. name: "",
  287. };
  288. }
  289. // 清除验证状态
  290. nextTick(() => {
  291. formRef.value?.clearValidate();
  292. phenologyFormRef.value?.clearValidate();
  293. });
  294. if(props.oldUser){
  295. currentStep.value = 2;
  296. getCurrentAndNextPhenology()
  297. }else{
  298. currentStep.value = 1;
  299. }
  300. }
  301. }
  302. );
  303. // 品种1变化时,重置品种2并触发验证
  304. const handleSpecieChange = (val) => {
  305. formData.value.typeIds = [];
  306. const specie = specieList.value.find(item => item.id === val);
  307. // 只有在用户没有手动修改名称时,才自动带出默认名称
  308. if (specie && !isNameEdited.value) {
  309. formData.value.name = city.value + specie.name + "农场";
  310. }
  311. getFruitsList(val);
  312. // 触发品种验证
  313. nextTick(() => {
  314. formRef.value?.validateField("speciesId");
  315. });
  316. };
  317. // 品种2变化时,触发验证
  318. const handleFruitsChange = () => {
  319. nextTick(() => {
  320. // 只校验当前字段(农场品类)
  321. formRef.value?.validateField("typeIds");
  322. });
  323. };
  324. // 农场名称输入时,标记为用户已手动修改
  325. const handleNameInput = () => {
  326. isNameEdited.value = true;
  327. };
  328. // 确认信息
  329. const handleConfirm = async () => {
  330. // 第一步:验证农场信息表单
  331. if (currentStep.value === 1) {
  332. if (!formRef.value) return;
  333. try {
  334. await formRef.value.validate();
  335. // 验证通过,获取物候期数据并切换到第二步
  336. await getCurrentAndNextPhenology();
  337. currentStep.value = 2;
  338. } catch (error) {
  339. console.log("表单验证失败", error);
  340. }
  341. }
  342. // 第二步:验证物候期表单并关闭弹窗
  343. else {
  344. if (!phenologyFormRef.value) return;
  345. try {
  346. await phenologyFormRef.value.validate();
  347. // 验证通过,提交所有数据并关闭弹窗
  348. const params = {
  349. ...formData.value,
  350. ...phenologyData.value,
  351. wkt: store.state.home.miniUserLocationPoint,
  352. expertMiniUserId: props.expertMiniUserId,
  353. containerId: specieList.value.find(item => item.id === formData.value.speciesId)?.defaultContainerId,
  354. }
  355. if(!props.oldUser){
  356. const { code, msg } = await VE_API.farm.saveFarm(params);
  357. if (code === 0) {
  358. ElMessage.success("农场信息确认成功");
  359. emit("update:show", false);
  360. } else {
  361. ElMessage.error(msg || '农场信息确认失败');
  362. }
  363. }else{
  364. ElMessage.success("农场信息确认成功");
  365. emit("update:show", false);
  366. }
  367. } catch (error) {
  368. console.log("物候期表单验证失败", error);
  369. }
  370. }
  371. };
  372. // 获取当前日期(YYYY-MM-DD格式)
  373. const getTodayDate = () => {
  374. const today = new Date();
  375. const year = today.getFullYear();
  376. const month = String(today.getMonth() + 1).padStart(2, "0");
  377. const day = String(today.getDate()).padStart(2, "0");
  378. return `${year}-${month}-${day}`;
  379. };
  380. // 物候期变化时,更新日期为对应物候期的 startDate
  381. const handlePhenologyChange = (phenologyId) => {
  382. const selectedPhenology = phenologyList.value.find(item => item.phenologyId === phenologyId);
  383. if (selectedPhenology && selectedPhenology.startDate) {
  384. phenologyData.value.phenologyStartDate = selectedPhenology.startDate;
  385. }
  386. };
  387. // 获取当前和下一个物候期
  388. const getCurrentAndNextPhenology = async () => {
  389. try {
  390. const { data } = await VE_API.home.getCurrentAndNextPhenology({
  391. expertMiniUserId: props.expertMiniUserId,
  392. containerId: specieList.value.find(item => item.id === formData.value.speciesId)?.defaultContainerId || '26',
  393. });
  394. if (data && Array.isArray(data)) {
  395. phenologyList.value = data;
  396. // 初始化物候期表单数据,日期使用第一个物候期的 startDate
  397. const firstPhenology = data[0];
  398. phenologyData.value = {
  399. phenologyId: firstPhenology?.phenologyId || "",
  400. phenologyStartDate: firstPhenology?.startDate || getTodayDate(),
  401. };
  402. }
  403. } catch (error) {
  404. console.error("获取物候期数据失败", error);
  405. ElMessage.error("获取物候期数据失败");
  406. }
  407. };
  408. </script>
  409. <style scoped lang="scss">
  410. .farm-info-popup {
  411. width: 100%;
  412. padding: 20px 16px;
  413. border-radius: 12px;
  414. .popup-title {
  415. font-size: 16px;
  416. color: #121212;
  417. }
  418. .farm-form {
  419. margin: 16px 0;
  420. background: rgba(33, 153, 248, 0.05);
  421. padding: 10px;
  422. border-radius: 5px;
  423. border: 1px solid rgba(33, 153, 248, 0.2);
  424. :deep(.el-form-item__label) {
  425. color: #1d2129;
  426. }
  427. .variety-select-wrap {
  428. display: flex;
  429. gap: 10px;
  430. width: 100%;
  431. }
  432. .unit {
  433. color: #666;
  434. font-size: 14px;
  435. padding-right: 8px;
  436. }
  437. }
  438. .btn-confirm {
  439. padding: 10px;
  440. background: #2199f8;
  441. color: #ffffff;
  442. border-radius: 4px;
  443. font-size: 16px;
  444. text-align: center;
  445. }
  446. }
  447. </style>