index.vue 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094
  1. <template>
  2. <div class="record-wrap">
  3. <custom-header :name="t('recordDetails.workName')"></custom-header>
  4. <div class="record-content">
  5. <div class="record-header" v-if="recordType === 'growth'">
  6. <span>{{ t('agriRecord.growthWorkName') }}</span>
  7. <div class="question">{{ currentGrowthAnomalyDetail?.interact_question }}</div>
  8. </div>
  9. <div class="record-header" v-else-if="recordType === 'pest'">
  10. <span>{{ t('agriRecord.pestWorkName') }}</span>
  11. <div class="question">{{ t('recordDetails.pestQuestion') }}</div>
  12. </div>
  13. <div class="record-header" v-else>
  14. <span>{{ t('agriRecord.phenologyWorkName') }}</span>
  15. <div class="question">{{ t('recordDetails.phenologyQuestion') }}</div>
  16. </div>
  17. <div class="record-body">
  18. <div class="card-wrap" v-if="recordType === 'growth'">
  19. <div class="card-item">
  20. <span class="item-label">{{ t('recordDetails.scienceLabel') }}</span>
  21. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  22. :collapse-text="expandCollapse.collapse" rows="3"
  23. :content="currentGrowthAnomalyDetail?.agri_judgment" />
  24. </div>
  25. <div class="tabs-list" v-if="!showMap">
  26. <div class="item-tab" :class="{ 'item-tab--active': activeGrowthAnomalyIndex === index }"
  27. v-for="(item, index) in growthAnomalyTabs" :key="index"
  28. @click="handleGrowthAnomalyTabClick(index)">{{ item.label }}
  29. </div>
  30. </div>
  31. <div class="card-item">
  32. <span class="item-label">{{ t('recordDetails.phenotypeLabel') }}</span>
  33. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  34. :collapse-text="expandCollapse.collapse" rows="3"
  35. :content="currentGrowthAnomalyDetail?.phenotype" />
  36. </div>
  37. <div class="card-item">
  38. <span class="item-label">{{ t('recordDetails.highRiskLabel') }}</span>
  39. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  40. :collapse-text="expandCollapse.collapse" rows="3"
  41. :content="currentGrowthAnomalyDetail?.patrol_points" />
  42. </div>
  43. </div>
  44. <div class="card-wrap" v-else-if="recordType === 'pest'">
  45. <div class="card-item">
  46. <span class="item-label">{{ t('recordDetails.scienceLabel') }}</span>
  47. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  48. :collapse-text="expandCollapse.collapse" rows="3"
  49. :content="t('recordDetails.pestScience')" />
  50. </div>
  51. <div class="pest-classify-picker">
  52. <div class="pest-classify-picker__row pest-classify-picker__row--top">
  53. <div v-for="(label, i) in pestTopLabels" :key="'top-' + i"
  54. class="pest-classify-picker__top-btn"
  55. :class="{ 'pest-classify-picker__top-btn--active': pestTopIndex === i }"
  56. @click="handlePestTopClick(i)">{{ label }}</div>
  57. </div>
  58. <div class="pest-classify-picker__row pest-classify-picker__row--grid4">
  59. <div v-for="(item, i) in pestCategoryLabels" :key="'cat-' + item.value"
  60. class="pest-classify-picker__chip pest-classify-picker__chip--solid"
  61. :class="{ 'pest-classify-picker__chip--solid-active': pestCategoryIndex === i }"
  62. @click="handlePestCategoryClick(i)">{{ item.label }}</div>
  63. </div>
  64. <div class="pest-classify-picker__row pest-classify-picker__row--grid4">
  65. <div v-for="(item, i) in pestDetailLabels" :key="'det-' + item.value"
  66. class="pest-classify-picker__chip pest-classify-picker__chip--soft"
  67. :class="{ 'pest-classify-picker__chip--soft-active': pestDetailIndex === i }"
  68. @click="handlePestDetailClick(i)">{{ item.label }}</div>
  69. </div>
  70. </div>
  71. <div class="card-item">
  72. <span class="item-label">{{ t('recordDetails.phenotypeLabel') }}</span>
  73. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  74. :collapse-text="expandCollapse.collapse" rows="3" :content="currentPestDetail?.phenotype" />
  75. </div>
  76. <div class="card-item">
  77. <span class="item-label">{{ t('recordDetails.highRiskLabel') }}</span>
  78. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  79. :collapse-text="expandCollapse.collapse" rows="3"
  80. :content="currentPestDetail?.patrol_points" />
  81. </div>
  82. </div>
  83. <div class="card-wrap" v-else>
  84. <div class="card-item">
  85. <span class="item-label">{{ t('recordDetails.farmAnalysisLabel') }}</span>
  86. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  87. :collapse-text="expandCollapse.collapse" rows="3"
  88. :content="t('recordDetails.phenologyAnalysis')" />
  89. </div>
  90. <div class="card-item">
  91. <span class="item-label">{{ t('recordDetails.patrolLabel') }}</span>
  92. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  93. :collapse-text="expandCollapse.collapse" rows="3"
  94. :content="workDetail.inspection_keypoints" />
  95. </div>
  96. <div class="card-item">
  97. <span class="item-label">{{ t('recordDetails.phenotypeLabel') }}</span>
  98. <text-ellipsis class="item-value" :expand-text="expandCollapse.expand"
  99. :collapse-text="expandCollapse.collapse" rows="3"
  100. :content="t('recordDetails.phenologyPhenotype')" />
  101. </div>
  102. </div>
  103. <!-- <div class="tabs-list" v-if="!showMap">
  104. <div class="item-tab" :class="{ 'item-tab--active': activeTab === index }"
  105. v-for="(item, index) in tabsList" :key="index" @click="handleTabClick(index)">{{ item.label }}
  106. </div>
  107. </div> -->
  108. <div class="card-wrap">
  109. <div class="border-wrap" v-if="showMap">
  110. <div class="question-box">
  111. <span>{{ workDetail.interaction_issue }}</span>
  112. <el-input class="input" v-model="input" size="large" type="number">
  113. <template #suffix>
  114. <span>%</span>
  115. </template>
  116. </el-input>
  117. </div>
  118. <div class="my-map">
  119. <div class="map-container" ref="mapContainer">
  120. <div class="tip">{{ t('recordDetails.drawTip') }}</div>
  121. </div>
  122. <div class="confirm-btn-wrap">
  123. <div class="cancel-btn">{{ t('recordDetails.notReached') }}</div>
  124. <div class="confirm-btn">{{ t('recordDetails.confirmUpload') }}</div>
  125. </div>
  126. </div>
  127. </div>
  128. <div v-if="!showMap && recordType === 'phenology'" class="border-wrap no-map-wrap">
  129. <div class="question-info">
  130. <span class="title">{{ t('recordDetails.keyAssessment') }}</span>
  131. <span class="content">{{ t('recordDetails.phenologyIssue') }}</span>
  132. <div class="current-status">{{ currentStatusText }}</div>
  133. </div>
  134. <div class="time-line">
  135. <GrowthStageTimeline v-model="growthStageIndex" :stages="growthStages"
  136. @scroll-settled="onStageScrollSettled" @locale-change="getFindPhenologyInfo" />
  137. <!-- <GrowthStageTimeline v-model="growthStageIndex" :stages="growthStages" /> -->
  138. </div>
  139. <div class="confirm-btn-wrap">
  140. <uploader @click="handleUploadClick" :before-read="beforeReadUpload" class="upload-wrap"
  141. multiple :max-count="10" :after-read="afterReadUpload">
  142. <div class="upload-btn">
  143. <el-icon>
  144. <Plus />
  145. </el-icon>
  146. <span>{{ t('recordDetails.uploadPhoto') }}</span>
  147. </div>
  148. </uploader>
  149. <div class="confirm-btn" @click="hanldeSubmit">{{ t('recordDetails.confirmInfo') }}</div>
  150. </div>
  151. </div>
  152. <div class="phenology-track-section">
  153. <PhenologyTrackTimelineItem :abnormalType="recordTypeObj[recordType]" />
  154. </div>
  155. </div>
  156. </div>
  157. </div>
  158. <!-- <div class="custom-bottom-fixed-btns">
  159. <div class="bottom-btn secondary-btn">{{ $t('转发记录') }}</div>
  160. </div> -->
  161. <!-- <div class="phenology-tip-banner">
  162. <div class="banner__left">
  163. <div class="banner__title">{{ $t('物候不整齐?') }}</div>
  164. <span class="banner__desc">
  165. 如果区域长势不同,会降低病虫害防治功效,
  166. 建议根据长势拆分区域,进行分区精细管理,
  167. 达到减药减肥的目的
  168. </span>
  169. </div>
  170. <div class="banner__btn" @click="goPartitionManage">
  171. 分区管理
  172. </div>
  173. </div> -->
  174. <div class="phenology-tip-banner" v-if="recordType !== 'phenology'">
  175. <div class="banner__left">
  176. <div class="banner__title">
  177. <el-icon size="17">
  178. <WarningFilled />
  179. </el-icon>
  180. <span>{{ t('recordDetails.abnormalBanner') }}</span>
  181. </div>
  182. <span class="banner__desc">
  183. {{ t('recordDetails.abnormalBannerDesc') }}
  184. </span>
  185. </div>
  186. <!-- <div class="banner__btn" @click="goPartitionManage">
  187. 分区管理
  188. </div> -->
  189. <div class="banner__btn" @click="handleAbnormalRecord">
  190. {{ t('recordDetails.abnormalRecord') }}
  191. </div>
  192. </div>
  193. <!-- <UploadProgressPopup ref="uploadProgressPopupRef" v-model:show="showUploadProgressPopup"
  194. :popup-image-upload-loading="popupImageUploadLoading" :init-img-arr="initImgArr"
  195. @cancel="handleCancelUploadPopup" @confirm="handleConfirmUpload" @handleUpload="handleUploadSuccess">
  196. <template #header>
  197. <div class="upload-progress-title">
  198. <span class="label">{{ $t('当前现状:') }}</span>
  199. <span class="value">{{ $t('60% 进入红黄叶进程') }}</span>
  200. </div>
  201. </template>
  202. </UploadProgressPopup> -->
  203. <UploadProgressPopup ref="uploadProgressPopupRef" v-model:show="showUploadProgressPopup"
  204. :popup-image-upload-loading="popupImageUploadLoading" :init-img-arr="initImgArr"
  205. :confirm-text="t('recordDetails.confirmUpload')" :upload-required="false"
  206. @reset="handleUploadPopupReset" @cancel="handleCancelUploadPopup" @confirm="handleConfirmUpload">
  207. <template #header>
  208. <div class="upload-form">
  209. <div class="form-item special-input">
  210. <div class="item-label" style="font-size: 14px;color: #5A5A5A;">
  211. <span>{{ t('recordDetails.abnormalCount') }}</span>
  212. </div>
  213. <el-input v-model="formData.ratio" type="number" size="large"
  214. :placeholder="t('recordDetails.inputPlaceholder')">
  215. <template #suffix>
  216. %
  217. </template>
  218. </el-input>
  219. </div>
  220. </div>
  221. </template>
  222. <template #footer>
  223. <div class="upload-form">
  224. <div class="form-item">
  225. <div class="item-label">{{ t('recordDetails.patrolDesc') }}</div>
  226. <el-input v-model="formData.regionName" size="large"
  227. :placeholder="t('recordDetails.inputDescPlaceholder')" />
  228. </div>
  229. <div class="map-container" ref="uploadMapContainer" @click="handleMapClick">
  230. <div class="tip" v-if="!hasMapConfirmGeometry">{{ t('recordDetails.drawTip') }}</div>
  231. </div>
  232. </div>
  233. </template>
  234. </UploadProgressPopup>
  235. </div>
  236. </template>
  237. <script setup>
  238. import customHeader from "@/components/customHeader.vue";
  239. import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
  240. import GrowthStageTimeline from "@/components/pageComponents/GrowthStageTimeline.vue";
  241. import UploadProgressPopup from "@/components/popup/UploadProgressPopup.vue";
  242. import { ref, onActivated, watch, nextTick, computed } from 'vue';
  243. import { useRouter, useRoute } from 'vue-router';
  244. import { useStore } from 'vuex';
  245. import { useI18n } from '@/i18n';
  246. import { RECORD_KEY_MAP } from '@/i18n/recordTextMap';
  247. import { Uploader, TextEllipsis } from "vant";
  248. import { ElMessage } from "element-plus";
  249. import UploadFile from "@/utils/upliadFile";
  250. import { getFileExt } from "@/utils/util";
  251. import IndexMap from "./map/index.js";
  252. import { Plus } from "@element-plus/icons-vue";
  253. const router = useRouter();
  254. const route = useRoute();
  255. const store = useStore();
  256. const { t } = useI18n();
  257. const TYPE_LEGACY_MAP = {
  258. 长势异常态势跟踪: 'growth',
  259. 病虫害态势监控: 'pest',
  260. 物候跟踪记录: 'phenology',
  261. };
  262. const recordType = computed(() => {
  263. const type = route.query.type;
  264. return TYPE_LEGACY_MAP[type] || type || 'phenology';
  265. });
  266. const expandCollapse = computed(() => ({
  267. expand: t('recordDetails.expand'),
  268. collapse: t('recordDetails.collapse'),
  269. }));
  270. function goPartitionManage() {
  271. router.push({ name: 'MapManage' });
  272. }
  273. const showMap = ref(false);
  274. /** 病虫害态势监控:分类选择(接入接口后可绑定 workDetail) */
  275. const pestTopIndex = ref(0);
  276. const pestCategoryIndex = ref(0);
  277. const pestDetailIndex = ref(0);
  278. const pestData = ref([]);
  279. const pestTopLabels = computed(() => [t('recordDetails.pestDisease'), t('recordDetails.pestInsect')]);
  280. const PEST_CATEGORY_DISEASE = [
  281. { key: 'catDiseaseFungal', value: 1 },
  282. { key: 'catDiseaseBacterial', value: 2 },
  283. { key: 'catDiseaseSoilBorne', value: 3 },
  284. { key: 'catDiseaseInsectBorne', value: 4 },
  285. { key: 'catDiseaseViral', value: 5 },
  286. ];
  287. const PEST_CATEGORY_INSECT = [
  288. { key: 'catInsectPiercing', value: 1 },
  289. { key: 'catInsectChewing', value: 2 },
  290. { key: 'catInsectBoring', value: 3 },
  291. { key: 'catInsectUnderground', value: 4 },
  292. ];
  293. const pestCategoryLabels = computed(() => {
  294. const list = pestTopIndex.value === 0 ? PEST_CATEGORY_DISEASE : PEST_CATEGORY_INSECT;
  295. return list.map(({ key, value }) => ({
  296. label: t(`recordDetails.${key}`),
  297. value,
  298. }));
  299. });
  300. const pestDetailLabels = computed(() =>
  301. pestData.value.map((item) => ({
  302. label: item.label ?? item.name ?? '',
  303. value: item.value ?? item.fourth_type ?? item.id,
  304. }))
  305. );
  306. const currentPestDetail = computed(() => pestData.value[pestDetailIndex.value]);
  307. /** 病虫害列表查询 */
  308. const getAbnormalPlan = async () => {
  309. const category = pestCategoryLabels.value[pestCategoryIndex.value];
  310. const params = {
  311. crop_type: JSON.parse(localStorage.getItem('selectedFarmData')).farm_variety,
  312. phenology_code: route.query.curCode,
  313. second_type: pestTopIndex.value === 0 ? 2 : 1,
  314. third_type: category?.value ?? 1,
  315. };
  316. const res = await VE_API.record.abnormalPlan(params);
  317. if (res.code === 200 && res.data?.length) {
  318. pestData.value = res.data;
  319. if (pestDetailIndex.value >= res.data.length) {
  320. pestDetailIndex.value = 0;
  321. }
  322. } else {
  323. pestData.value = [];
  324. pestDetailIndex.value = 0;
  325. }
  326. };
  327. /** 病虫害分类选择 */
  328. const handlePestTopClick = async (index) => {
  329. pestTopIndex.value = index;
  330. pestCategoryIndex.value = 0;
  331. pestDetailIndex.value = 0;
  332. await getAbnormalPlan();
  333. };
  334. const handlePestCategoryClick = async (index) => {
  335. pestCategoryIndex.value = index;
  336. pestDetailIndex.value = 0;
  337. await getAbnormalPlan();
  338. };
  339. const handlePestDetailClick = (index) => {
  340. pestDetailIndex.value = index;
  341. };
  342. const indexMap = new IndexMap();
  343. const mapContainer = ref(null);
  344. const uploadMapContainer = ref(null);
  345. const location = ref(null);
  346. const input = ref('');
  347. const tabsList = ref([
  348. { label: '分区一', value: 0 },
  349. { label: '分区二', value: 1 },
  350. { label: '分区三', value: 2 },
  351. { label: '分区四', value: 3 },
  352. ]);
  353. const activeGrowthAnomalyIndex = ref(0);
  354. const growthAnomalyData = ref([]);
  355. const growthAnomalyTabs = computed(() =>
  356. growthAnomalyData.value.map((item, index) => ({
  357. label: item.name ?? '',
  358. value: item.value ?? item.id ?? index,
  359. }))
  360. );
  361. const currentGrowthAnomalyDetail = computed(() => growthAnomalyData.value[activeGrowthAnomalyIndex.value]);
  362. const handleGrowthAnomalyTabClick = (index) => {
  363. activeGrowthAnomalyIndex.value = index;
  364. };
  365. const currentStatusText = computed(() =>
  366. t('recordDetails.currentStatus', {
  367. label: curStage.value?.label || '',
  368. period: curStage.value?.periodTitle || '',
  369. })
  370. );
  371. const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
  372. const mapConfirmPayload = computed(() => store.state.recordDetails.mapConfirmPayload);
  373. const hasMapConfirmGeometry = computed(() => {
  374. const coordinates = mapConfirmPayload.value?.coordinates;
  375. return Array.isArray(coordinates) && coordinates.length > 0;
  376. });
  377. const formData = ref({
  378. ratio: '',
  379. regionName: '',
  380. });
  381. function destroyUploadMap() {
  382. if (indexMap.kmap?.destroy) {
  383. indexMap.kmap.destroy();
  384. }
  385. indexMap.kmap = null;
  386. }
  387. function ensureUploadMap() {
  388. if (!uploadMapContainer.value) return false;
  389. location.value = location.value || DEFAULT_MAP_LOCATION;
  390. destroyUploadMap();
  391. indexMap.initMap(location.value, uploadMapContainer.value, { editable: false, movable: false });
  392. return Boolean(indexMap.kmap);
  393. }
  394. function resizeUploadMap() {
  395. indexMap.kmap?.map?.updateSize?.();
  396. }
  397. /** 弹窗动画结束后再初始化,避免容器尺寸为 0 导致白屏 */
  398. function syncUploadMapView() {
  399. const { coordinates } = mapConfirmPayload.value;
  400. nextTick(() => {
  401. setTimeout(() => {
  402. if (!ensureUploadMap()) return;
  403. resizeUploadMap();
  404. if (Array.isArray(coordinates) && coordinates.length) {
  405. indexMap.setAreaGeometry(coordinates);
  406. resizeUploadMap();
  407. }
  408. }, 250);
  409. });
  410. }
  411. function syncFormFromMapConfirmPayload() {
  412. if (!showUploadProgressPopup.value) return;
  413. syncUploadMapView();
  414. }
  415. const handleAbnormalRecord = () => {
  416. formData.value.ratio = '';
  417. formData.value.regionName = '';
  418. showUploadProgressPopup.value = true;
  419. };
  420. const handleMapClick = () => {
  421. router.push({
  422. name: 'MapManage', query: {
  423. type: 'pest',
  424. }
  425. });
  426. }
  427. const showUploadProgressPopup = ref(false);
  428. watch(showUploadProgressPopup, (show) => {
  429. if (show) {
  430. syncUploadMapView();
  431. } else {
  432. destroyUploadMap();
  433. }
  434. });
  435. const initImgArr = ref([]);
  436. const uploadProgressPopupRef = ref(null);
  437. const popupImageUploadLoading = ref(false);
  438. const uploadFileObj = new UploadFile();
  439. const miniUserId = localStorage.getItem("MINI_USER_ID");
  440. const handleUploadPopupReset = () => {
  441. initImgArr.value = [];
  442. popupImageUploadLoading.value = false;
  443. };
  444. const beforeReadUpload = (file) => {
  445. showUploadProgressPopup.value = true;
  446. return true;
  447. };
  448. const curStage = ref({});
  449. const curCode = ref('')
  450. function onStageScrollSettled(index, stage, code) {
  451. if (stage) curStage.value = stage;
  452. curCode.value = code ?? stage?.phenophase_code ?? "";
  453. }
  454. /** 从节点文案如「60%展开」中取出百分数,转为 0~1,如 60% -> 0.6 */
  455. function labelPercentToProgress(label) {
  456. if (typeof label !== "string" || !label.trim()) return 0;
  457. const m = label.match(/(\d+(?:\.\d+)?)\s*%/);
  458. if (!m) return 0;
  459. return Number(m[1]) / 100;
  460. }
  461. const hanldeSubmit = () => {
  462. const farmData = JSON.parse(localStorage.getItem('selectedFarmData'))
  463. const time = new Date().toISOString()
  464. const params = {
  465. farm_id: farmData.farm_id,
  466. farm_variety: farmData.farm_variety,
  467. current_code: curCode.value,
  468. new_start_date: time.slice(0, 10),
  469. progress: labelPercentToProgress(curStage.value.label),
  470. }
  471. VE_API.monitor.farmPhenologyAdjust(params).then(res => {
  472. if (res.code === 200) {
  473. ElMessage.success(t('recordDetails.confirmSuccess'))
  474. }
  475. })
  476. }
  477. const afterReadUpload = async (data) => {
  478. initImgArr.value = [];
  479. if (!Array.isArray(data)) {
  480. data = [data];
  481. }
  482. popupImageUploadLoading.value = true;
  483. try {
  484. for (const file of data) {
  485. const fileVal = file.file;
  486. file.status = "uploading";
  487. file.message = "上传中...";
  488. const ext = getFileExt(fileVal.name);
  489. const key = `birdseye-look-mini/${miniUserId}/${new Date().getTime()}.${ext}`;
  490. const resFilename = await uploadFileObj.put(key, fileVal);
  491. if (resFilename) {
  492. file.status = "done";
  493. file.message = "";
  494. initImgArr.value.push(resFilename);
  495. } else {
  496. file.status = "failed";
  497. file.message = "上传失败";
  498. ElMessage.error("图片上传失败,请稍后再试!");
  499. }
  500. }
  501. } finally {
  502. popupImageUploadLoading.value = false;
  503. }
  504. };
  505. const handleCancelUploadPopup = () => {
  506. showUploadProgressPopup.value = false;
  507. store.commit('recordDetails/CLEAR_MAP_CONFIRM_PAYLOAD');
  508. };
  509. const recordTypeObj = {
  510. phenology: 0,
  511. pest: 1,
  512. growth: 2,
  513. };
  514. function validateAbnormalRatio() {
  515. const ratio = String(formData.value.ratio ?? '').trim();
  516. if (ratio === '' || Number.isNaN(Number(ratio))) {
  517. ElMessage.warning(t('recordDetails.ratioRequired'));
  518. return false;
  519. }
  520. return true;
  521. }
  522. const handleConfirmUpload = (imgArr) => {
  523. if (!validateAbnormalRatio()) return;
  524. if (recordType.value === 'pest') {
  525. const coordinates = mapConfirmPayload.value?.coordinates;
  526. const hasDrawnMap = Array.isArray(coordinates) && coordinates.length > 0;
  527. if (!hasDrawnMap) {
  528. handleMapClick();
  529. return;
  530. }
  531. }
  532. const farmData = JSON.parse(localStorage.getItem('selectedFarmData'));
  533. const params = {
  534. farm_id: farmData.farm_id,
  535. variety_code: farmData.farm_variety,
  536. category_code: farmData.farm_category,
  537. type: recordTypeObj[recordType.value],
  538. time: new Date().toISOString().slice(0, 10),
  539. zone_name: mapConfirmPayload.value.zone_name,
  540. coordinates: mapConfirmPayload.value.coordinates[0],
  541. "recog_type": "20",
  542. image_urls: imgArr,
  543. ...formData.value,
  544. }
  545. VE_API.record.writeFarmRecord(params).then(res => {
  546. if (res.code === 200) {
  547. showUploadProgressPopup.value = false;
  548. store.commit('recordDetails/CLEAR_MAP_CONFIRM_PAYLOAD');
  549. ElMessage.success(t('recordDetails.confirmSuccess'));
  550. } else {
  551. ElMessage.error(res.msg);
  552. }
  553. });
  554. };
  555. const handleUploadClick = () => { };
  556. const activeTab = ref(0);
  557. const handleTabClick = (index) => {
  558. activeTab.value = index;
  559. };
  560. /** 生育期进程时间轴:接入接口后可替换为接口数据 */
  561. /** 不设初值时由 GrowthStageTimeline 默认选中间一档 */
  562. const growthStageIndex = ref();
  563. const growthStages = ref([]);
  564. /** 与 GrowthStageTimeline 默认档一致:未绑 v-model 时用中间索引 */
  565. function syncCurStageFromModel() {
  566. const stages = growthStages.value;
  567. if (!Array.isArray(stages) || !stages.length) return;
  568. const n = stages.length;
  569. let i = growthStageIndex.value;
  570. if (i === undefined || i === null) {
  571. i = Math.max(0, Math.floor((n - 1) / 2));
  572. } else {
  573. i = Math.min(Math.max(0, i), n - 1);
  574. }
  575. const stage = stages[i];
  576. if (stage) curStage.value = stage;
  577. curCode.value = stage?.phenophase_code ?? "";
  578. }
  579. watch([growthStages, growthStageIndex], syncCurStageFromModel, { immediate: true });
  580. onActivated(() => {
  581. sessionStorage.removeItem('mapManageConfirmPayload');
  582. syncFormFromMapConfirmPayload();
  583. if (route.query.workId) {
  584. getWorkDetail();
  585. }
  586. if (route.query.type === 'pest') {
  587. getAbnormalPlan();
  588. }
  589. if (route.query.type === 'growth') {
  590. getGrowthAnomalyInfo();
  591. }
  592. getFindPhenologyInfo()
  593. if (showMap.value) {
  594. location.value = "POINT(113.6142086995688 23.585836479509055)";
  595. indexMap.initMap(location.value, mapContainer.value);
  596. }
  597. });
  598. const workDetail = ref({});
  599. const getWorkDetail = async () => {
  600. const res = await VE_API.monitor.getWorkDetail({ id: route.query.workId });
  601. if (res.code === 200 && res.data.length) {
  602. workDetail.value = res.data[0];
  603. }
  604. }
  605. // 获取生长异常列表
  606. const getGrowthAnomalyInfo = async () => {
  607. const farmData = JSON.parse(localStorage.getItem('selectedFarmData'));
  608. const params = {
  609. crop_type: farmData.farm_variety,
  610. phenology_code: route.query.curCode,
  611. farm_id: farmData.farm_id,
  612. }
  613. const res = await VE_API.record.growthAnomalyInfo(params);
  614. if (res.code === 200 && res.data?.length) {
  615. growthAnomalyData.value = res.data;
  616. activeGrowthAnomalyIndex.value = 0;
  617. } else {
  618. growthAnomalyData.value = [];
  619. activeGrowthAnomalyIndex.value = 0;
  620. }
  621. }
  622. function normalizeTimeDescribe(raw) {
  623. if (Array.isArray(raw)) return raw;
  624. if (typeof raw === 'string' && raw.trim()) {
  625. try {
  626. const parsed = JSON.parse(raw);
  627. return Array.isArray(parsed) ? parsed : [];
  628. } catch {
  629. return [];
  630. }
  631. }
  632. return [];
  633. }
  634. const getFindPhenologyInfo = async () => {
  635. const cropType = JSON.parse(localStorage.getItem('selectedFarmData')).farm_variety;
  636. const res = await VE_API.monitor.getFindPhenologyInfo({ crop_type: cropType });
  637. if (res.code === 200 && Array.isArray(res.data) && res.data.length) {
  638. const flat = [];
  639. for (const item of res.data) {
  640. const milestones = normalizeTimeDescribe(item.time_discribe);
  641. for (const t of milestones) {
  642. flat.push({
  643. label: t.label ?? '',
  644. tags: Array.isArray(t.tags) ? t.tags : [],
  645. periodTitle: item.phenophase_name ?? '',
  646. periodSubtitle: item.phenophase_discribe ?? '',
  647. phenophase_code: item.phenophase_code ?? item.phenophaseCode ?? '',
  648. });
  649. }
  650. }
  651. if (flat.length) {
  652. growthStages.value = flat;
  653. }
  654. }
  655. };
  656. </script>
  657. <style scoped lang="scss">
  658. .record-wrap {
  659. height: 100vh;
  660. background: #f2f3f5;
  661. .record-content {
  662. // height: calc(100% - 120px);
  663. // overflow: auto;
  664. // padding-bottom: 120px;
  665. height: calc(100% - 40px);
  666. overflow: auto;
  667. // padding-bottom: 120px;
  668. .record-header {
  669. color: #fff;
  670. background: #2199F8;
  671. padding: 10px;
  672. font-size: 22px;
  673. .question {
  674. font-size: 14px;
  675. color: #2199F8;
  676. margin-top: 10px;
  677. padding: 4px 5px;
  678. border-radius: 2px;
  679. background: #ffffff;
  680. }
  681. }
  682. .record-body {
  683. padding: 10px;
  684. .tabs-list {
  685. margin: 10px 0;
  686. flex-shrink: 0;
  687. display: grid;
  688. grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
  689. gap: 8px;
  690. .item-tab {
  691. box-sizing: border-box;
  692. padding: 6px 4px;
  693. border-radius: 2px;
  694. color: #767676;
  695. background: #fff;
  696. display: flex;
  697. align-items: center;
  698. justify-content: center;
  699. text-align: center;
  700. font-size: 13px;
  701. white-space: nowrap;
  702. word-break: keep-all;
  703. }
  704. .item-tab--active {
  705. background: #2199F8;
  706. color: #ffffff;
  707. }
  708. }
  709. .card-wrap {
  710. background: #fff;
  711. border-radius: 8px;
  712. padding: 10px;
  713. .card-item {
  714. padding: 5px 10px;
  715. border-radius: 5px;
  716. background: #F6F6F6;
  717. .item-label {
  718. color: rgba(60, 60, 60, 0.5);
  719. }
  720. .item-value {
  721. display: inline;
  722. }
  723. }
  724. .card-item+.card-item {
  725. margin-top: 8px;
  726. }
  727. .pest-classify-picker {
  728. margin: 8px 0;
  729. padding: 10px;
  730. border-radius: 8px;
  731. background: #f2f3f5;
  732. &__row {
  733. display: flex;
  734. justify-content: center;
  735. gap: 8px;
  736. &--top {
  737. .pest-classify-picker__top-btn {
  738. padding: 6px 30px;
  739. border-radius: 6px;
  740. background: #E9E9E9;
  741. color: #767676;
  742. }
  743. .pest-classify-picker__top-btn--active {
  744. background: #ffffff;
  745. color: #1a1a1a;
  746. }
  747. }
  748. &--grid4 {
  749. margin-top: 10px;
  750. display: grid;
  751. grid-template-columns: repeat(4, 1fr);
  752. gap: 8px;
  753. }
  754. }
  755. &__chip {
  756. border: none;
  757. padding: 6px 4px;
  758. border-radius: 6px;
  759. font-size: 13px;
  760. text-align: center;
  761. white-space: nowrap;
  762. overflow: hidden;
  763. text-overflow: ellipsis;
  764. background: #ffffff;
  765. color: #909090;
  766. }
  767. &__chip--solid-active {
  768. background: #2199f8;
  769. color: #ffffff;
  770. }
  771. &__chip--soft-active {
  772. background: rgba(33, 153, 248, 0.14);
  773. color: #2199f8;
  774. }
  775. }
  776. .border-wrap {
  777. border-radius: 8px;
  778. border: 0.5px solid #2199F8;
  779. padding: 10px;
  780. .question-box {
  781. border: 1px solid #2199F8;
  782. border-radius: 5px;
  783. padding: 8px;
  784. color: #5A5A5A;
  785. .input {
  786. margin-top: 10px;
  787. width: 100%;
  788. }
  789. }
  790. .confirm-btn-wrap {
  791. display: flex;
  792. gap: 13px;
  793. div {
  794. flex: 1;
  795. text-align: center;
  796. border-radius: 5px;
  797. font-size: 16px;
  798. padding: 8px 0;
  799. }
  800. .cancel-btn {
  801. border: 1px solid #D6D6D6;
  802. color: #7F7F7F;
  803. }
  804. .confirm-btn {
  805. border: 1px solid #2199F8;
  806. background: #2199F8;
  807. color: #fff;
  808. }
  809. }
  810. .question-info {
  811. padding: 8px 10px;
  812. border-radius: 5px;
  813. background: rgba(33, 153, 248, 0.1);
  814. color: #909090;
  815. font-weight: 500;
  816. .content {
  817. font-weight: 400;
  818. }
  819. .current-status {
  820. color: #2199F8;
  821. }
  822. }
  823. .time-line {
  824. margin: 10px 0;
  825. }
  826. }
  827. .no-map-wrap {
  828. .confirm-btn-wrap {
  829. div {
  830. font-size: 14px;
  831. flex: none;
  832. }
  833. .upload-wrap {
  834. width: calc(100% - 96px - 13px);
  835. padding: 0;
  836. background: rgba(255, 149, 61, 0.1);
  837. color: #FF953D;
  838. border-radius: 4px;
  839. display: flex;
  840. justify-content: center;
  841. border: 1px solid #FF953D;
  842. .upload-btn {
  843. display: flex;
  844. align-items: center;
  845. justify-content: center;
  846. gap: 3px;
  847. }
  848. }
  849. .confirm-btn {
  850. width: 96px;
  851. }
  852. }
  853. }
  854. }
  855. .card-wrap+.card-wrap {
  856. margin-top: 10px;
  857. }
  858. .phenology-track-section {
  859. margin-top: 15px;
  860. display: flex;
  861. flex-direction: column;
  862. gap: 16px;
  863. }
  864. }
  865. }
  866. .phenology-tip-banner {
  867. position: fixed;
  868. left: 12px;
  869. right: 12px;
  870. // bottom: calc(90px + env(safe-area-inset-bottom, 0px));
  871. bottom: 20px;
  872. z-index: 99;
  873. display: flex;
  874. align-items: center;
  875. justify-content: space-between;
  876. gap: 8px;
  877. padding: 10px 14px;
  878. border-radius: 12px;
  879. // background: linear-gradient(180deg, #CCE8FF 0%, #ffffff 60%);
  880. background: linear-gradient(180deg, #FFDFC5 -12.59%, #FFFFFF 38.15%);
  881. box-shadow: 0 2px 8px rgba(33, 153, 248, 0.12);
  882. .banner__left {
  883. flex: 1;
  884. .banner__title {
  885. display: flex;
  886. align-items: center;
  887. gap: 4px;
  888. font-size: 16px;
  889. font-weight: 600;
  890. // color: #1890ff;
  891. color: #FF953D;
  892. }
  893. .banner__desc {
  894. font-size: 12px;
  895. color: rgba(0, 0, 0, 0.4);
  896. }
  897. }
  898. .banner__btn {
  899. padding: 7px 16px;
  900. border-radius: 25px;
  901. color: #ffffff;
  902. background: linear-gradient(180deg, #FFB273 0%, #FF953D 100%);
  903. // background: linear-gradient(180deg, #5cb8ff 0%, #2e90ff 100%);
  904. }
  905. }
  906. }
  907. // .my-map {
  908. // width: 100%;
  909. // .map-container {
  910. // width: 100%;
  911. // height: 170px;
  912. // margin: 10px 0;
  913. // clip-path: inset(0px round 5px);
  914. // position: relative;
  915. // .tip {
  916. // position: absolute;
  917. // top: 0;
  918. // left: 0;
  919. // color: #fff;
  920. // padding: 5px 10px;
  921. // border-radius: 5px;
  922. // background: rgba(0, 0, 0, 0.6);
  923. // z-index: 2;
  924. // }
  925. // }
  926. // }
  927. .map-container {
  928. width: 100%;
  929. height: 170px;
  930. margin: 10px 0;
  931. clip-path: inset(0px round 5px);
  932. position: relative;
  933. .tip {
  934. position: absolute;
  935. top: 0;
  936. left: 0;
  937. color: #fff;
  938. padding: 5px 10px;
  939. border-radius: 5px;
  940. background: rgba(0, 0, 0, 0.6);
  941. z-index: 2;
  942. }
  943. }
  944. .upload-progress-title {
  945. color: rgba(60, 60, 60, 0.5);
  946. font-weight: 500;
  947. background: rgba(33, 153, 248, 0.1);
  948. border-radius: 5px;
  949. padding: 5px 10px;
  950. margin-bottom: 12px;
  951. .value {
  952. color: #2199F8;
  953. }
  954. }
  955. .upload-form {
  956. .form-item {
  957. margin-bottom: 10px;
  958. .item-label {
  959. font-size: 16px;
  960. margin-bottom: 8px;
  961. .optional {
  962. font-size: 14px;
  963. color: rgba(0, 0, 0, 0.2);
  964. }
  965. }
  966. }
  967. .special-input {
  968. padding: 8px;
  969. background: rgba(33, 153, 248, 0.1);
  970. border-radius: 5px;
  971. border: 1px solid #2199F8;
  972. }
  973. }
  974. </style>