index.vue 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553
  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">
  6. <span>{{ t(recordHeaderInfo.titleKey) }}</span>
  7. <div class="question">{{ recordHeaderInfo.question }}</div>
  8. </div>
  9. <div class="record-body">
  10. <div class="card-wrap">
  11. <div class="card-item">
  12. <span class="item-label">{{ t(`recordDetails.${leadDetailField.labelKey}`) }}</span>
  13. <text-ellipsis class="item-value" v-bind="textEllipsisBind"
  14. :content="leadDetailField.content" />
  15. </div>
  16. <div v-if="recordType === 'pest'" class="phenology-tip-banner">
  17. <div class="banner__left">
  18. <div class="banner__title">
  19. <span>{{ t('recordDetails.abnormalBanner') }}</span>
  20. </div>
  21. <span class="banner__desc">{{ t('recordDetails.abnormalBannerDesc') }}</span>
  22. </div>
  23. <div class="banner__btn" @click="handleAbnormalRecord">
  24. <el-icon size="17">
  25. <CameraFilled />
  26. </el-icon>
  27. <span>{{ t('recordDetails.abnormalRecord') }}</span>
  28. </div>
  29. </div>
  30. <div v-if="recordType === 'growth'" class="phenology-tip-banner">
  31. <div class="banner__left">
  32. <div class="banner__title">
  33. <span>{{ t('recordDetails.growthAbnormalBanner') }}</span>
  34. </div>
  35. <span class="banner__desc">{{ t('recordDetails.growthAbnormalBannerDesc') }}</span>
  36. </div>
  37. <div class="banner__btn" @click="handleAbnormalRecord">
  38. <span>{{ t('recordDetails.growthAbnormalRecord') }}</span>
  39. </div>
  40. </div>
  41. <template v-if="recordType === 'phenology'">
  42. <div v-for="field in trailDetailFields" :key="field.labelKey" class="card-item">
  43. <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
  44. <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
  45. </div>
  46. <div class="phenology-tip-banner blue">
  47. <div class="banner__left">
  48. <div class="banner__title">{{ t('recordDetails.partitionBannerTitle') }}</div>
  49. <span class="banner__desc">{{ t('recordDetails.partitionBannerDesc') }}</span>
  50. </div>
  51. <div class="banner__btn" @click="goPartitionManage">
  52. {{ t('recordDetails.partitionManage') }}
  53. </div>
  54. </div>
  55. </template>
  56. </div>
  57. <template v-if="recordType === 'growth' && !showMap">
  58. <div class="growth-anomaly-tabs">
  59. <div v-for="(item, index) in growthAnomalyTabs" :key="index" class="item-tab"
  60. :class="{ 'item-tab--active': activeGrowthAnomalyIndex === index }"
  61. @click="handleGrowthAnomalyTabClick(index)">
  62. <span v-if="getGrowthTabDisplayLevel(index) > 0" class="item-tab__badge" :class="[
  63. `item-tab__badge--${getGrowthTabDisplayLevel(index)}`,
  64. { 'item-tab__badge--on-active': activeGrowthAnomalyIndex === index },
  65. ]">
  66. {{ t('agriRecord.riskLevel', { level: getGrowthTabDisplayLevel(index) }) }}
  67. </span>
  68. <span class="item-tab__text">{{ item.label }}</span>
  69. </div>
  70. </div>
  71. <div class="card-wrap growth-detail-card">
  72. <div v-for="field in trailDetailFields" :key="field.labelKey" class="card-item"
  73. :class="{ 'card-item--phenotype': field.image }">
  74. <div v-if="field.image" class="card-item__phenotype">
  75. <div class="card-item__phenotype-content">
  76. <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
  77. <text-ellipsis class="item-value" v-bind="textEllipsisBind"
  78. :content="field.content" />
  79. </div>
  80. <!-- <img class="card-item__phenotype-image" :src="field.image" alt=""
  81. @click="handlePhenotypeImagePreview(field.image)" /> -->
  82. </div>
  83. <template v-else>
  84. <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
  85. <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
  86. </template>
  87. </div>
  88. </div>
  89. </template>
  90. <div v-if="recordType === 'pest'" class="pest-classify-picker">
  91. <div class="pest-classify-picker__tabs" ref="pestTabsRef">
  92. <div v-for="(label, i) in pestTopLabels" :key="'top-' + i" :ref="(el) => setPestTabRef(el, i)"
  93. class="pest-classify-picker__tab"
  94. :class="{ 'pest-classify-picker__tab--active': pestTopIndex === i }"
  95. @click="handlePestTopClick(i)">{{ label }}</div>
  96. </div>
  97. <div class="pest-classify-picker__panel" ref="pestPanelRef"
  98. :style="{ '--arrow-left': `${pestPanelArrowLeft}px` }">
  99. <div class="pest-classify-picker__categories">
  100. <div v-for="(item, i) in pestCategoryLabels" :key="'cat-' + item.value"
  101. class="pest-classify-picker__category"
  102. :class="{ 'pest-classify-picker__category--active': pestCategoryIndex === i }"
  103. @click="handlePestCategoryClick(i)">{{ item.label }}</div>
  104. </div>
  105. <div class="pest-classify-picker__details">
  106. <div v-for="(item, i) in pestDetailLabels" :key="'det-' + item.value"
  107. class="pest-classify-picker__detail"
  108. :class="{ 'pest-classify-picker__detail--active': pestDetailIndex === i }"
  109. @click="handlePestDetailClick(i)">
  110. <span v-if="item.risk_level > 0" class="pest-classify-picker__badge"
  111. :class="`pest-classify-picker__badge--${item.risk_level}`">
  112. {{ t('agriRecord.riskLevel', { level: item.risk_level }) }}
  113. </span>
  114. <span class="pest-classify-picker__detail-text">{{ item.label }}</span>
  115. </div>
  116. </div>
  117. </div>
  118. </div>
  119. <div v-if="recordType === 'pest'" class="card-wrap pest-detail-card">
  120. <div v-for="field in trailDetailFields" :key="field.labelKey" class="card-item"
  121. :class="{ 'card-item--phenotype': field.image }">
  122. <div v-if="field.image" class="card-item__phenotype">
  123. <div class="card-item__phenotype-content">
  124. <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
  125. <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
  126. </div>
  127. <!-- <img class="card-item__phenotype-image" :src="field.image" alt=""
  128. @click="handlePhenotypeImagePreview(field.image)" /> -->
  129. </div>
  130. <template v-else>
  131. <span class="item-label">{{ t(`recordDetails.${field.labelKey}`) }}</span>
  132. <text-ellipsis class="item-value" v-bind="textEllipsisBind" :content="field.content" />
  133. </template>
  134. </div>
  135. </div>
  136. <!-- <div class="tabs-list" v-if="showMap">
  137. <div class="item-tab" :class="{ 'item-tab--active': activeTab === index }"
  138. v-for="(item, index) in tabsList" :key="index" @click="handleTabClick(index)">{{ item.label }}
  139. </div>
  140. </div> -->
  141. <div class="card-wrap">
  142. <div class="border-wrap" v-if="showMap">
  143. <div class="question-box">
  144. <span>{{ workDetail.interaction_issue }}</span>
  145. <el-input class="input" v-model="input" size="large" type="number">
  146. <template #suffix>
  147. <span>%</span>
  148. </template>
  149. </el-input>
  150. </div>
  151. <div class="my-map">
  152. <div class="map-container" ref="recordMapContainer">
  153. <div class="tip">{{ t('recordDetails.drawTip') }}</div>
  154. </div>
  155. <div class="confirm-btn-wrap">
  156. <div class="cancel-btn">{{ t('recordDetails.notReached') }}</div>
  157. <div class="confirm-btn">{{ t('recordDetails.confirmUpload') }}</div>
  158. </div>
  159. </div>
  160. </div>
  161. <div v-if="!showMap && recordType === 'phenology'" class="border-wrap no-map-wrap">
  162. <div class="question-info">
  163. <span class="title">{{ t('recordDetails.keyAssessment') }}</span>
  164. <span class="content">{{ workDetail.interaction_issue }}</span>
  165. <div class="current-status">{{ currentStatusText }}</div>
  166. </div>
  167. <div class="time-line">
  168. <GrowthStageTimeline :key="phenologyTimelineKey" v-model="growthStageIndex"
  169. :stages="growthStages" @scroll-settled="onStageScrollSettled"
  170. @locale-change="getFindPhenologyInfo" />
  171. </div>
  172. <div class="confirm-btn-wrap">
  173. <div class="confirm-btn" @click="hanldeSubmit">{{ t('recordDetails.confirmInfo') }}</div>
  174. </div>
  175. </div>
  176. <div class="phenology-track-section">
  177. <PhenologyTrackTimelineItem ref="phenologyTrackTimelineRef"
  178. :abnormalType="recordTypeObj[recordType]" />
  179. </div>
  180. </div>
  181. </div>
  182. </div>
  183. <!-- <div class="custom-bottom-fixed-btns">
  184. <div class="bottom-btn secondary-btn">{{ $t('转发记录') }}</div>
  185. </div> -->
  186. <RiskAssessPopup v-model:show="showRiskAssessPopup" />
  187. <!-- 表型特征图片大图预览 -->
  188. <ImagePreviewPopup v-model:show="showPhenotypeImagePreview" :images="phenotypePreviewImages" />
  189. <!-- 物候 上传弹窗 -->
  190. <UploadProgressPopup ref="phenologyUploadPopupRef" v-model:show="showPhenologyUploadPopup"
  191. :enable-identify="false" @cancel="handleCancelPhenologyUploadPopup" :upload-required="false"
  192. @confirm="handleConfirmPhenologyUpload">
  193. <template #header>
  194. <div class="upload-progress-title">
  195. <!-- <span class="label">{{ $t('当前现状:') }}</span> -->
  196. <span class="value">{{ currentStatusText }}</span>
  197. </div>
  198. </template>
  199. </UploadProgressPopup>
  200. <!-- 病虫害 上传弹窗 -->
  201. <UploadProgressPopup ref="pestUploadPopupRef" v-model:show="showPestUploadPopup" :enable-identify="true"
  202. :init-img-arr="pestInitImgArr" :confirm-text="t('recordDetails.confirmUpload')" :upload-required="false"
  203. @reset="handlePestUploadPopupReset" @cancel="handleCancelPestUploadPopup"
  204. @confirm="handleConfirmPestUpload">
  205. <template #header>
  206. <div class="upload-form">
  207. <div v-if="recordType === 'growth'" class="form-item growth-abnormal-type-field">
  208. <div class="growth-abnormal-type-field__label">
  209. {{ t('recordDetails.growthAbnormalTypeLabel') }}
  210. </div>
  211. <el-select v-model="formData.abnormalType" class="growth-abnormal-type-select"
  212. placeholder="请选择异常类型" size="large">
  213. <el-option v-for="item in growthAbnormalTypeOptions" :key="item.value" :label="item.label"
  214. :value="item.value" />
  215. </el-select>
  216. </div>
  217. <div class="form-item special-input">
  218. <div class="item-label" style="font-size: 14px;color: #5A5A5A;">
  219. <span>{{ t('recordDetails.abnormalCount') }}</span>
  220. </div>
  221. <el-input v-model="formData.ratio" type="number" size="large"
  222. :placeholder="t('recordDetails.inputPlaceholder')">
  223. <template #suffix>
  224. {{ t('recordDetails.treeUnit') }}
  225. </template>
  226. </el-input>
  227. </div>
  228. </div>
  229. </template>
  230. <template #footer>
  231. <div class="upload-form">
  232. <div class="form-item">
  233. <div class="item-label">{{ t('recordDetails.patrolDesc') }}</div>
  234. <el-input v-model="formData.regionName" size="large"
  235. :placeholder="t('recordDetails.inputDescPlaceholder')" />
  236. </div>
  237. <div class="map-container" ref="uploadMapContainer" @click="handleMapClick">
  238. <div class="tip" v-if="!hasMapConfirmGeometry">{{ t('recordDetails.drawTip') }}</div>
  239. </div>
  240. </div>
  241. </template>
  242. </UploadProgressPopup>
  243. </div>
  244. </template>
  245. <script setup>
  246. import customHeader from "@/components/customHeader.vue";
  247. import PhenologyTrackTimelineItem from "@/components/pageComponents/PhenologyTrackTimelineItem.vue";
  248. import GrowthStageTimeline from "@/components/pageComponents/GrowthStageTimeline.vue";
  249. import UploadProgressPopup from "@/components/popup/UploadProgressPopup.vue";
  250. import ImagePreviewPopup from "@/components/popup/ImagePreviewPopup.vue";
  251. import RiskAssessPopup from "./components/RiskAssessPopup.vue";
  252. import { ref, onActivated, watch, nextTick, computed } from 'vue';
  253. import { useRouter, useRoute } from 'vue-router';
  254. import { useStore } from 'vuex';
  255. import { useI18n } from '@/i18n';
  256. import { RECORD_KEY_MAP } from '@/i18n/recordTextMap';
  257. import { TextEllipsis } from "vant";
  258. import { ElMessage } from "element-plus";
  259. import IndexMap from "./map/index.js";
  260. import pestPhenotypePlaceholder from '@/assets/img/common/sd-1.jpg';
  261. import pestPreviewImg2 from '@/assets/img/common/sd-2.jpg';
  262. import pestPreviewImg3 from '@/assets/img/common/sd-3.jpg';
  263. /** 表型大图预览 mock 多图(联调后可删) */
  264. const PHENOTYPE_PREVIEW_MOCK_IMAGES = [
  265. pestPhenotypePlaceholder,
  266. pestPreviewImg2,
  267. pestPreviewImg3,
  268. ];
  269. const router = useRouter();
  270. const route = useRoute();
  271. const store = useStore();
  272. const { t } = useI18n();
  273. const TYPE_LEGACY_MAP = {
  274. 长势异常态势跟踪: 'growth',
  275. 病虫害态势监控: 'pest',
  276. 物候跟踪记录: 'phenology',
  277. };
  278. const recordType = computed(() => {
  279. const type = route.query.type;
  280. return TYPE_LEGACY_MAP[type] || type || 'phenology';
  281. });
  282. const expandCollapse = computed(() => ({
  283. expand: t('recordDetails.expand'),
  284. collapse: t('recordDetails.collapse'),
  285. }));
  286. const textEllipsisBind = computed(() => ({
  287. expandText: expandCollapse.value.expand,
  288. collapseText: expandCollapse.value.collapse,
  289. rows: 3,
  290. }));
  291. function goPartitionManage() {
  292. router.push({ name: 'MapManage' });
  293. }
  294. const showMap = ref(false);
  295. const showRiskAssessPopup = ref(false);
  296. /** 表型特征图片预览弹窗 */
  297. const showPhenotypeImagePreview = ref(false);
  298. const phenotypePreviewImages = ref([]);
  299. /** 点击表型缩略图,打开大图预览 */
  300. function handlePhenotypeImagePreview(image) {
  301. if (!image) return;
  302. phenotypePreviewImages.value = PHENOTYPE_PREVIEW_MOCK_IMAGES;
  303. showPhenotypeImagePreview.value = true;
  304. }
  305. /** 病虫害态势监控:分类选择(接入接口后可绑定 workDetail) */
  306. const pestTopIndex = ref(0);
  307. const pestCategoryIndex = ref(0);
  308. const pestDetailIndex = ref(0);
  309. const pestData = ref([]);
  310. const pestTopLabels = computed(() => [t('recordDetails.pestDisease'), t('recordDetails.pestInsect')]);
  311. const PEST_CATEGORY_DISEASE = [
  312. { key: 'catDiseaseFungal', value: 1 },
  313. { key: 'catDiseaseBacterial', value: 2 },
  314. { key: 'catDiseaseSoilBorne', value: 3 },
  315. { key: 'catDiseaseInsectBorne', value: 4 },
  316. { key: 'catDiseaseViral', value: 5 },
  317. ];
  318. const PEST_CATEGORY_INSECT = [
  319. { key: 'catInsectPiercing', value: 1 },
  320. { key: 'catInsectChewing', value: 2 },
  321. { key: 'catInsectBoring', value: 3 },
  322. { key: 'catInsectUnderground', value: 4 },
  323. ];
  324. const pestCategoryLabels = computed(() => {
  325. const list = pestTopIndex.value === 0 ? PEST_CATEGORY_DISEASE : PEST_CATEGORY_INSECT;
  326. return list.map(({ key, value }) => ({
  327. label: t(`recordDetails.${key}`),
  328. value,
  329. }));
  330. });
  331. const PEST_MOCK_RISK_LEVELS = [3, 2, 1, 0];
  332. const pestDetailLabels = computed(() =>
  333. pestData.value.map((item, index) => ({
  334. label: item.label ?? item.name ?? '',
  335. value: item.value ?? item.fourth_type ?? item.id,
  336. risk_level: PEST_MOCK_RISK_LEVELS[index % PEST_MOCK_RISK_LEVELS.length],
  337. }))
  338. );
  339. const currentPestDetail = computed(() => pestData.value[pestDetailIndex.value]);
  340. /** 病虫害列表查询 */
  341. const getAbnormalPlan = async () => {
  342. const category = pestCategoryLabels.value[pestCategoryIndex.value];
  343. const params = {
  344. crop_type: JSON.parse(localStorage.getItem('selectedFarmData')).farm_variety,
  345. phenology_code: route.query.curCode,
  346. second_type: pestTopIndex.value === 0 ? 2 : 1,
  347. third_type: category?.value ?? 1,
  348. };
  349. const res = await VE_API.record.abnormalPlan(params);
  350. if (res.code === 200 && res.data?.length) {
  351. pestData.value = res.data;
  352. if (pestDetailIndex.value >= res.data.length) {
  353. pestDetailIndex.value = 0;
  354. }
  355. } else {
  356. pestData.value = [];
  357. pestDetailIndex.value = 0;
  358. }
  359. };
  360. /** 病虫害分类选择 */
  361. const handlePestTopClick = async (index) => {
  362. pestTopIndex.value = index;
  363. pestCategoryIndex.value = 0;
  364. pestDetailIndex.value = 0;
  365. await getAbnormalPlan();
  366. syncPestPanelArrow();
  367. };
  368. const handlePestCategoryClick = async (index) => {
  369. pestCategoryIndex.value = index;
  370. pestDetailIndex.value = 0;
  371. await getAbnormalPlan();
  372. };
  373. const handlePestDetailClick = (index) => {
  374. pestDetailIndex.value = index;
  375. };
  376. const pestTabsRef = ref(null);
  377. const pestTabRefs = ref([]);
  378. const pestPanelRef = ref(null);
  379. const pestPanelArrowLeft = ref(0);
  380. const setPestTabRef = (el, index) => {
  381. if (el) {
  382. pestTabRefs.value[index] = el;
  383. }
  384. };
  385. const syncPestPanelArrow = () => {
  386. nextTick(() => {
  387. requestAnimationFrame(() => {
  388. const activeTab = pestTabRefs.value[pestTopIndex.value];
  389. const panel = pestPanelRef.value;
  390. if (!activeTab || !panel) return;
  391. const panelRect = panel.getBoundingClientRect();
  392. const tabRect = activeTab.getBoundingClientRect();
  393. pestPanelArrowLeft.value = tabRect.left + tabRect.width / 2 - panelRect.left;
  394. });
  395. });
  396. };
  397. watch(pestTopIndex, syncPestPanelArrow);
  398. watch(recordType, (type) => {
  399. if (type === 'pest') {
  400. syncPestPanelArrow();
  401. }
  402. });
  403. const indexMap = new IndexMap();
  404. const recordMapContainer = ref(null);
  405. const uploadMapContainer = ref(null);
  406. const location = ref(null);
  407. const input = ref('');
  408. const tabsList = ref([
  409. { label: '分区一', value: 0 },
  410. { label: '分区二', value: 1 },
  411. { label: '分区三', value: 2 },
  412. { label: '分区四', value: 3 },
  413. ]);
  414. /** UI 占位:接口未返回等级时,前三个 Tab 依次展示三级 / 二级 / 一级 */
  415. const GROWTH_TAB_MOCK_LEVELS = [3, 2, 1];
  416. function getGrowthTabDisplayLevel(index) {
  417. return GROWTH_TAB_MOCK_LEVELS[index] ?? 0;
  418. }
  419. function resolveGrowthAnomalyOptionValue(item, index) {
  420. const raw = item?.value ?? item?.id ?? item?.code ?? index;
  421. if (raw === undefined || raw === null || raw === '') return '';
  422. return String(raw);
  423. }
  424. const activeGrowthAnomalyIndex = ref(0);
  425. const growthAnomalyData = ref([]);
  426. const growthAnomalyTabs = computed(() =>
  427. growthAnomalyData.value.map((item, index) => ({
  428. label: item.name ?? '',
  429. value: resolveGrowthAnomalyOptionValue(item, index),
  430. }))
  431. );
  432. const currentGrowthAnomalyDetail = computed(() => growthAnomalyData.value[activeGrowthAnomalyIndex.value]);
  433. const handleGrowthAnomalyTabClick = (index) => {
  434. activeGrowthAnomalyIndex.value = index;
  435. };
  436. const currentStatusText = computed(() =>
  437. t('recordDetails.currentStatus', {
  438. label: curStage.value?.label || '',
  439. period: curStage.value?.periodTitle || '',
  440. })
  441. );
  442. const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
  443. function getFarmMapLocation() {
  444. try {
  445. const farmData = JSON.parse(localStorage.getItem('selectedFarmData') || '{}');
  446. return farmData.geom_wkt || farmData.wkt || DEFAULT_MAP_LOCATION;
  447. } catch {
  448. return DEFAULT_MAP_LOCATION;
  449. }
  450. }
  451. const mapConfirmPayload = computed(() => store.state.recordDetails.mapConfirmPayload);
  452. const hasMapConfirmGeometry = computed(() => store.getters['recordDetails/mapConfirmCoordinatesArray'].length > 0);
  453. const formData = ref({
  454. ratio: '',
  455. regionName: '',
  456. abnormalType: '',
  457. });
  458. const growthAbnormalTypeOptions = computed(() => {
  459. if (growthAnomalyData.value.length) {
  460. return growthAnomalyData.value.map((item, index) => ({
  461. label: item.name ?? '',
  462. value: resolveGrowthAnomalyOptionValue(item, index),
  463. }));
  464. }
  465. return [
  466. { label: t('recordDetails.tabFruitBlocked'), value: 'fruit_blocked' },
  467. { label: t('recordDetails.tabSlowGrowth'), value: 'slow_growth' },
  468. { label: t('recordDetails.tabVigorous'), value: 'vigorous' },
  469. { label: t('recordDetails.tabWilting'), value: 'wilting' },
  470. ];
  471. });
  472. function syncGrowthAbnormalTypeSelection() {
  473. const idx = activeGrowthAnomalyIndex.value;
  474. const fromApi = resolveGrowthAnomalyOptionValue(growthAnomalyData.value[idx], idx);
  475. const options = growthAbnormalTypeOptions.value;
  476. if (fromApi && options.some((o) => o.value === fromApi)) {
  477. formData.value.abnormalType = fromApi;
  478. return;
  479. }
  480. formData.value.abnormalType = options[idx]?.value ?? options[0]?.value ?? '';
  481. }
  482. function destroyMap() {
  483. if (indexMap.kmap?.destroy) {
  484. indexMap.kmap.destroy();
  485. }
  486. indexMap.kmap = null;
  487. }
  488. function ensureRecordMap() {
  489. if (!recordMapContainer.value) return false;
  490. location.value = getFarmMapLocation();
  491. destroyMap();
  492. indexMap.initMap(location.value, recordMapContainer.value, { editable: false, movable: false });
  493. return Boolean(indexMap.kmap);
  494. }
  495. function syncRecordMapView() {
  496. nextTick(() => {
  497. setTimeout(() => {
  498. if (showMap.value) return;
  499. if (!ensureRecordMap()) return;
  500. resizeMap();
  501. }, 250);
  502. });
  503. }
  504. function ensureUploadMap() {
  505. if (!uploadMapContainer.value) return false;
  506. location.value = getFarmMapLocation();
  507. destroyMap();
  508. indexMap.initMap(location.value, uploadMapContainer.value, { editable: false, movable: false });
  509. return Boolean(indexMap.kmap);
  510. }
  511. function resizeMap() {
  512. indexMap.kmap?.map?.updateSize?.();
  513. }
  514. /** 弹窗动画结束后再初始化,避免容器尺寸为 0 导致白屏 */
  515. function syncUploadMapView() {
  516. const geometryArr = store.getters['recordDetails/mapConfirmCoordinatesArray'];
  517. nextTick(() => {
  518. setTimeout(() => {
  519. if (!ensureUploadMap()) return;
  520. resizeMap();
  521. if (geometryArr.length) {
  522. indexMap.setAreaGeometry(geometryArr);
  523. resizeMap();
  524. }
  525. }, 250);
  526. });
  527. }
  528. function syncFormFromMapConfirmPayload() {
  529. if (!showPestUploadPopup.value) return;
  530. syncUploadMapView();
  531. }
  532. const handleAbnormalRecord = () => {
  533. formData.value.ratio = '';
  534. formData.value.regionName = '';
  535. if (recordType.value === 'growth') {
  536. syncGrowthAbnormalTypeSelection();
  537. } else {
  538. formData.value.abnormalType = '';
  539. }
  540. showPestUploadPopup.value = true;
  541. };
  542. const handleMapClick = () => {
  543. router.push({
  544. name: 'MapManage', query: {
  545. type: recordType.value,
  546. }
  547. });
  548. }
  549. const showPhenologyUploadPopup = ref(false);
  550. const showPestUploadPopup = ref(false);
  551. watch(showPestUploadPopup, (show) => {
  552. if (show) {
  553. if (recordType.value === 'growth') {
  554. nextTick(() => syncGrowthAbnormalTypeSelection());
  555. }
  556. syncUploadMapView();
  557. } else {
  558. destroyMap();
  559. if (!showMap.value) {
  560. syncRecordMapView();
  561. }
  562. }
  563. });
  564. const pestInitImgArr = ref([]);
  565. const phenologyUploadPopupRef = ref(null);
  566. const pestUploadPopupRef = ref(null);
  567. const phenologyTrackTimelineRef = ref(null);
  568. const handlePestUploadPopupReset = () => {
  569. pestInitImgArr.value = [];
  570. };
  571. const curStage = ref({});
  572. const curCode = ref('')
  573. function onStageScrollSettled(index, stage, code) {
  574. shouldApplyPhenologyDefault.value = false;
  575. if (stage) curStage.value = stage;
  576. curCode.value = code ?? stage?.phenophase_code ?? "";
  577. }
  578. /** 从节点文案如「60%展开」中取出百分数,转为 0~1,如 60% -> 0.6 */
  579. function labelPercentToProgress(label) {
  580. if (typeof label !== "string" || !label.trim()) return 0;
  581. const m = label.match(/(\d+(?:\.\d+)?)\s*%/);
  582. if (!m) return 0;
  583. return Number(m[1]) / 100;
  584. }
  585. const hanldeSubmit = () => {
  586. showPhenologyUploadPopup.value = true;
  587. };
  588. const handleCancelPhenologyUploadPopup = () => {
  589. showPhenologyUploadPopup.value = false;
  590. };
  591. const handleConfirmPhenologyUpload = () => {
  592. const farmData = JSON.parse(localStorage.getItem('selectedFarmData'));
  593. const time = new Date().toISOString();
  594. const params = {
  595. farm_id: farmData.farm_id,
  596. farm_variety: farmData.farm_variety,
  597. current_code: curCode.value,
  598. new_start_date: time.slice(0, 10),
  599. progress: labelPercentToProgress(curStage.value.label),
  600. };
  601. VE_API.monitor.farmPhenologyAdjust(params).then(res => {
  602. if (res.code === 200) {
  603. showPhenologyUploadPopup.value = false;
  604. phenologyTrackTimelineRef.value?.refresh?.();
  605. ElMessage.success(t('recordDetails.confirmSuccess'));
  606. }
  607. });
  608. };
  609. const handleCancelPestUploadPopup = () => {
  610. showPestUploadPopup.value = false;
  611. store.commit('recordDetails/CLEAR_MAP_CONFIRM_PAYLOAD');
  612. };
  613. const recordTypeObj = {
  614. phenology: 0,
  615. pest: 1,
  616. growth: 2,
  617. };
  618. function validateAbnormalRatio() {
  619. const ratio = String(formData.value.ratio ?? '').trim();
  620. if (ratio === '' || Number.isNaN(Number(ratio))) {
  621. ElMessage.warning(t('recordDetails.ratioRequired'));
  622. return false;
  623. }
  624. return true;
  625. }
  626. function validateGrowthAbnormalType() {
  627. if (String(formData.value.abnormalType ?? '').trim() === '') {
  628. ElMessage.warning(t('recordDetails.growthAbnormalTypeRequired'));
  629. return false;
  630. }
  631. return true;
  632. }
  633. const handleConfirmPestUpload = (imgArr) => {
  634. if (!validateAbnormalRatio()) return;
  635. if (recordType.value === 'growth' && !validateGrowthAbnormalType()) return;
  636. if (recordType.value === 'pest') {
  637. const hasDrawnMap = store.getters['recordDetails/mapConfirmCoordinatesArray'].length > 0;
  638. if (!hasDrawnMap) {
  639. handleMapClick();
  640. return;
  641. }
  642. }
  643. const farmData = JSON.parse(localStorage.getItem('selectedFarmData'));
  644. const params = {
  645. farm_id: farmData.farm_id,
  646. variety_code: farmData.farm_variety,
  647. category_code: farmData.farm_category,
  648. type: recordTypeObj[recordType.value],
  649. time: new Date().toISOString().slice(0, 10),
  650. zone_name: mapConfirmPayload.value.zone_name,
  651. coordinates: mapConfirmPayload.value.coordinates,
  652. "recog_type": "20",
  653. image_urls: imgArr,
  654. ...formData.value,
  655. }
  656. VE_API.record.writeFarmRecord(params).then(res => {
  657. if (res.code === 200) {
  658. showPestUploadPopup.value = false;
  659. store.commit('recordDetails/CLEAR_MAP_CONFIRM_PAYLOAD');
  660. phenologyTrackTimelineRef.value?.refresh?.();
  661. ElMessage.success(t('recordDetails.confirmSuccess'));
  662. } else {
  663. ElMessage.error(res.msg);
  664. }
  665. });
  666. };
  667. const activeTab = ref(0);
  668. const handleTabClick = (index) => {
  669. activeTab.value = index;
  670. };
  671. const workDetail = ref({});
  672. const recordHeaderInfo = computed(() => {
  673. if (recordType.value === 'growth') {
  674. return {
  675. titleKey: 'agriRecord.growthWorkName',
  676. question: currentGrowthAnomalyDetail.value?.interact_question,
  677. };
  678. }
  679. if (recordType.value === 'pest') {
  680. return {
  681. titleKey: 'agriRecord.pestWorkName',
  682. question: currentPestDetail.value?.interaction_reason,
  683. };
  684. }
  685. return {
  686. titleKey: 'agriRecord.phenologyWorkName',
  687. question: workDetail.value.work_reason_short,
  688. };
  689. });
  690. const leadDetailField = computed(() => {
  691. if (recordType.value === 'growth') {
  692. return {
  693. labelKey: 'scienceLabel',
  694. content: currentGrowthAnomalyDetail.value?.agri_judgment,
  695. };
  696. }
  697. if (recordType.value === 'pest') {
  698. return {
  699. labelKey: 'scienceLabel',
  700. content: currentPestDetail.value?.kepu,
  701. };
  702. }
  703. return {
  704. labelKey: 'farmAnalysisLabel',
  705. content: workDetail.value.work_reason_detail,
  706. };
  707. });
  708. const trailDetailFields = computed(() => {
  709. if (recordType.value === 'growth') {
  710. const detail = currentGrowthAnomalyDetail.value;
  711. const phenotypeImage = detail?.phenotype_image ?? detail?.phenotype_img ?? detail?.image;
  712. const mockLevel = getGrowthTabDisplayLevel(activeGrowthAnomalyIndex.value);
  713. const anomalyLevelContent =
  714. detail?.abnormal_level ??
  715. detail?.anomaly_level ??
  716. detail?.level_desc ??
  717. detail?.harm_level ??
  718. detail?.level;
  719. return [
  720. {
  721. labelKey: 'anomalyLevelLabel',
  722. content:
  723. anomalyLevelContent ||
  724. (mockLevel ? t('agriRecord.riskLevel', { level: mockLevel }) : ''),
  725. },
  726. {
  727. labelKey: 'phenotypeLabel',
  728. content: detail?.phenotype,
  729. image: phenotypeImage || pestPhenotypePlaceholder,
  730. },
  731. { labelKey: 'highRiskLabel', content: detail?.patrol_points },
  732. ];
  733. }
  734. if (recordType.value === 'pest') {
  735. const detail = currentPestDetail.value;
  736. return [
  737. {
  738. labelKey: 'harmLevelLabel',
  739. content: detail?.harm ?? detail?.harm_desc ?? detail?.harm_level ?? detail?.damage,
  740. },
  741. {
  742. labelKey: 'phenotypeLabel',
  743. content: detail?.phenotype,
  744. image: pestPhenotypePlaceholder,
  745. },
  746. { labelKey: 'highRiskLabel', content: detail?.patrol_points },
  747. ];
  748. }
  749. return [
  750. { labelKey: 'patrolLabel', content: workDetail.value.inspection_keypoints },
  751. { labelKey: 'phenotypeLabel', content: workDetail.value.phenotypic_characteristics },
  752. ];
  753. });
  754. const growthStageIndex = ref();
  755. const growthStages = ref([]);
  756. const phenologyRawList = ref([]);
  757. const phenologyTimelineKey = ref(0);
  758. /** 数据加载后按物候编码定位,用户拖动后不再自动覆盖 */
  759. const shouldApplyPhenologyDefault = ref(true);
  760. function normalizePhenologyCode(code) {
  761. if (code == null || code === '') return '';
  762. return String(code).trim();
  763. }
  764. function findStageIndexByPhenophaseCode(stages, code) {
  765. const target = normalizePhenologyCode(code);
  766. if (!Array.isArray(stages) || !stages.length || !target) return -1;
  767. return stages.findIndex(
  768. (s) => normalizePhenologyCode(s.phenophase_code) === target
  769. );
  770. }
  771. function resolveStageIndexByPhenophaseCode(stages, rawItems, code) {
  772. const target = normalizePhenologyCode(code);
  773. if (!target || !Array.isArray(stages) || !stages.length) return -1;
  774. const byFlat = findStageIndexByPhenophaseCode(stages, target);
  775. if (byFlat >= 0) return byFlat;
  776. const period = (rawItems || []).find(
  777. (item) =>
  778. normalizePhenologyCode(item.phenophase_code ?? item.phenophaseCode) ===
  779. target
  780. );
  781. if (!period) return -1;
  782. const milestones = normalizeTimeDescribe(period.time_discribe);
  783. const firstLabel = milestones[0]?.label;
  784. if (!firstLabel) return -1;
  785. return stages.findIndex(
  786. (s) =>
  787. s.label === firstLabel &&
  788. normalizePhenologyCode(s.phenophase_code) === target
  789. );
  790. }
  791. function buildGrowthStagesFlat(rawItems) {
  792. const flat = [];
  793. for (const item of rawItems) {
  794. const periodCode =
  795. item.phenophase_code ?? item.phenophaseCode ?? '';
  796. const milestones = normalizeTimeDescribe(item.time_discribe);
  797. for (const t of milestones) {
  798. flat.push({
  799. label: t.label ?? '',
  800. tags: Array.isArray(t.tags) ? t.tags : [],
  801. periodTitle: item.phenophase_name ?? '',
  802. periodSubtitle: item.phenophase_discribe ?? '',
  803. phenophase_code:
  804. t.phenophase_code ??
  805. t.phenophaseCode ??
  806. t.code ??
  807. periodCode,
  808. });
  809. }
  810. }
  811. return flat;
  812. }
  813. /** 时间轴初始定位:优先 workDetail.phenelogy_code,否则路由 curCode */
  814. function resolveTimelineDefaultCode() {
  815. const fromWork =
  816. workDetail.value?.phenelogy_code ?? workDetail.value?.phenology_code;
  817. return normalizePhenologyCode(fromWork ?? route.query.curCode);
  818. }
  819. function applyTimelineDefaultIndex(flat, rawItems) {
  820. const targetCode = resolveTimelineDefaultCode();
  821. const defaultIndex = resolveStageIndexByPhenophaseCode(
  822. flat,
  823. rawItems,
  824. targetCode
  825. );
  826. if (defaultIndex < 0) return -1;
  827. shouldApplyPhenologyDefault.value = true;
  828. growthStageIndex.value = defaultIndex;
  829. growthStages.value = flat;
  830. phenologyTimelineKey.value += 1;
  831. return defaultIndex;
  832. }
  833. function syncCurStageFromModel() {
  834. const stages = growthStages.value;
  835. if (!Array.isArray(stages) || !stages.length) return;
  836. const n = stages.length;
  837. let i;
  838. if (shouldApplyPhenologyDefault.value) {
  839. const targetCode = resolveTimelineDefaultCode();
  840. const byCode = resolveStageIndexByPhenophaseCode(
  841. stages,
  842. phenologyRawList.value,
  843. targetCode
  844. );
  845. i = byCode >= 0 ? byCode : Math.max(0, Math.floor((n - 1) / 2));
  846. if (byCode >= 0) {
  847. growthStageIndex.value = i;
  848. shouldApplyPhenologyDefault.value = false;
  849. }
  850. } else {
  851. i = growthStageIndex.value;
  852. if (i === undefined || i === null) {
  853. i = Math.max(0, Math.floor((n - 1) / 2));
  854. } else {
  855. i = Math.min(Math.max(0, i), n - 1);
  856. }
  857. }
  858. const stage = stages[i];
  859. if (stage) curStage.value = stage;
  860. curCode.value = stage?.phenophase_code ?? "";
  861. }
  862. watch([growthStages, growthStageIndex], syncCurStageFromModel, { immediate: true });
  863. onActivated(async () => {
  864. sessionStorage.removeItem('mapManageConfirmPayload');
  865. syncFormFromMapConfirmPayload();
  866. if (route.query.workId) {
  867. await getWorkDetail();
  868. }
  869. if (route.query.type === 'pest') {
  870. await getAbnormalPlan();
  871. syncPestPanelArrow();
  872. }
  873. if (route.query.type === 'growth') {
  874. getGrowthAnomalyInfo();
  875. }
  876. getFindPhenologyInfo();
  877. if (!showMap.value) {
  878. syncRecordMapView();
  879. }
  880. });
  881. const getWorkDetail = async () => {
  882. const res = await VE_API.monitor.getWorkDetail({ id: route.query.workId });
  883. if (res.code === 200 && res.data.length) {
  884. workDetail.value = res.data[0];
  885. console.log(workDetail.value);
  886. }
  887. }
  888. // 获取生长异常列表
  889. const getGrowthAnomalyInfo = async () => {
  890. const farmData = JSON.parse(localStorage.getItem('selectedFarmData'));
  891. const params = {
  892. crop_type: farmData.farm_variety,
  893. phenology_code: route.query.curCode,
  894. farm_id: farmData.farm_id,
  895. }
  896. const res = await VE_API.record.growthAnomalyInfo(params);
  897. if (res.code === 200 && res.data?.length) {
  898. growthAnomalyData.value = res.data;
  899. activeGrowthAnomalyIndex.value = 0;
  900. } else {
  901. growthAnomalyData.value = [];
  902. activeGrowthAnomalyIndex.value = 0;
  903. }
  904. }
  905. function normalizeTimeDescribe(raw) {
  906. if (Array.isArray(raw)) return raw;
  907. if (typeof raw === 'string' && raw.trim()) {
  908. try {
  909. const parsed = JSON.parse(raw);
  910. return Array.isArray(parsed) ? parsed : [];
  911. } catch {
  912. return [];
  913. }
  914. }
  915. return [];
  916. }
  917. const getFindPhenologyInfo = async () => {
  918. const cropType = JSON.parse(localStorage.getItem('selectedFarmData')).farm_variety;
  919. const res = await VE_API.monitor.getFindPhenologyInfo({ crop_type: cropType });
  920. if (res.code === 200 && Array.isArray(res.data) && res.data.length) {
  921. phenologyRawList.value = res.data;
  922. const flat = buildGrowthStagesFlat(res.data);
  923. if (!flat.length) return;
  924. const defaultIndex = applyTimelineDefaultIndex(flat, res.data);
  925. await nextTick();
  926. if (defaultIndex >= 0) {
  927. growthStageIndex.value = defaultIndex;
  928. shouldApplyPhenologyDefault.value = false;
  929. syncCurStageFromModel();
  930. }
  931. }
  932. };
  933. </script>
  934. <style scoped lang="scss">
  935. .record-wrap {
  936. height: 100vh;
  937. background: #f2f3f5;
  938. .record-content {
  939. // height: calc(100% - 120px);
  940. // overflow: auto;
  941. // padding-bottom: 120px;
  942. height: calc(100% - 40px);
  943. overflow: auto;
  944. // padding-bottom: 120px;
  945. .record-header {
  946. color: #fff;
  947. background: #2199F8;
  948. padding: 10px;
  949. font-size: 22px;
  950. .question {
  951. font-size: 14px;
  952. color: #2199F8;
  953. margin-top: 10px;
  954. padding: 4px 5px;
  955. border-radius: 2px;
  956. background: #ffffff;
  957. }
  958. }
  959. .record-body {
  960. padding: 10px;
  961. .tabs-list {
  962. margin: 10px 0;
  963. flex-shrink: 0;
  964. }
  965. .card-wrap+.phenology-tip-banner,
  966. .phenology-tip-banner+.growth-anomaly-tabs,
  967. .growth-anomaly-tabs+.card-wrap {
  968. margin-top: 10px;
  969. }
  970. .growth-anomaly-tabs {
  971. display: flex;
  972. flex-wrap: wrap;
  973. gap: 8px;
  974. padding-top: 12px;
  975. .item-tab {
  976. position: relative;
  977. flex: 1 1 calc(33.333% - 6px);
  978. min-width: 0;
  979. box-sizing: border-box;
  980. padding: 8px 0;
  981. border: 1px solid transparent;
  982. border-radius: 6px;
  983. background: #fff;
  984. color: #666;
  985. text-align: center;
  986. font-size: 14px;
  987. font-weight: 600;
  988. }
  989. .item-tab--active {
  990. border-color: #2199f8;
  991. background: rgba(33, 153, 248, 0.08);
  992. color: #2199f8;
  993. }
  994. .item-tab__text {
  995. display: block;
  996. width: 100%;
  997. }
  998. .item-tab__badge {
  999. position: absolute;
  1000. top: -9px;
  1001. right: -8px;
  1002. padding: 0 6px;
  1003. border-radius: 2px;
  1004. font-size: 10px;
  1005. font-weight: 400;
  1006. line-height: 16px;
  1007. color: #fff;
  1008. z-index: 1;
  1009. &--1 {
  1010. background: rgba(33, 153, 248, 0.4);
  1011. }
  1012. &--2 {
  1013. background: rgba(33, 153, 248, 0.7);
  1014. }
  1015. &--3 {
  1016. background: #2199f8;
  1017. }
  1018. &--on-active {
  1019. background: #2199f8;
  1020. }
  1021. }
  1022. }
  1023. .card-wrap {
  1024. background: #fff;
  1025. border-radius: 8px;
  1026. padding: 10px;
  1027. .card-item {
  1028. padding: 5px 10px;
  1029. border-radius: 5px;
  1030. background: #F6F6F6;
  1031. .item-label {
  1032. color: rgba(60, 60, 60, 0.5);
  1033. }
  1034. .item-value {
  1035. display: inline;
  1036. }
  1037. &__phenotype {
  1038. display: flex;
  1039. align-items: flex-start;
  1040. gap: 6px;
  1041. &-content {
  1042. flex: 1;
  1043. min-width: 0;
  1044. }
  1045. }
  1046. &__phenotype-image {
  1047. width: 70px;
  1048. height: 63px;
  1049. object-fit: cover;
  1050. border-radius: 5px;
  1051. cursor: pointer;
  1052. }
  1053. }
  1054. .card-item+.card-item {
  1055. margin-top: 8px;
  1056. }
  1057. .border-wrap {
  1058. border-radius: 8px;
  1059. border: 0.5px solid #2199F8;
  1060. padding: 10px;
  1061. .question-box {
  1062. border: 1px solid #2199F8;
  1063. background: rgba(33, 153, 248, 0.1);
  1064. border-radius: 5px;
  1065. padding: 8px;
  1066. color: #5A5A5A;
  1067. .input {
  1068. margin-top: 10px;
  1069. width: 100%;
  1070. }
  1071. }
  1072. .confirm-btn-wrap {
  1073. display: flex;
  1074. gap: 13px;
  1075. div {
  1076. flex: 1;
  1077. text-align: center;
  1078. border-radius: 5px;
  1079. font-size: 16px;
  1080. padding: 8px 0;
  1081. }
  1082. .cancel-btn {
  1083. border: 1px solid #D6D6D6;
  1084. color: #7F7F7F;
  1085. }
  1086. .confirm-btn {
  1087. border: 1px solid #2199F8;
  1088. background: #2199F8;
  1089. color: #fff;
  1090. }
  1091. }
  1092. .question-info {
  1093. padding: 8px 10px;
  1094. border-radius: 5px;
  1095. background: rgba(33, 153, 248, 0.1);
  1096. color: #909090;
  1097. font-weight: 500;
  1098. .content {
  1099. font-weight: 400;
  1100. }
  1101. .current-status {
  1102. color: #2199F8;
  1103. }
  1104. }
  1105. .time-line {
  1106. margin: 10px 0;
  1107. }
  1108. }
  1109. }
  1110. .card-wrap+.card-wrap {
  1111. margin-top: 10px;
  1112. }
  1113. .pest-classify-picker {
  1114. padding: 10px;
  1115. background: #f2f3f5;
  1116. border-radius: 8px;
  1117. display: flex;
  1118. flex-direction: column;
  1119. align-items: center;
  1120. &__tabs {
  1121. position: relative;
  1122. z-index: 2;
  1123. display: inline-flex;
  1124. width: fit-content;
  1125. padding: 3px;
  1126. border-radius: 4px;
  1127. background: #fff;
  1128. box-sizing: border-box;
  1129. }
  1130. &__tab {
  1131. position: relative;
  1132. padding: 6px 28px;
  1133. border-radius: 4px;
  1134. font-size: 13px;
  1135. line-height: 18px;
  1136. color: #767676;
  1137. text-align: center;
  1138. box-sizing: border-box;
  1139. &--active {
  1140. background: #2199f8;
  1141. color: #fff;
  1142. }
  1143. }
  1144. &__panel {
  1145. position: relative;
  1146. align-self: stretch;
  1147. width: 100%;
  1148. margin-top: 10px;
  1149. padding: 12px 10px 10px;
  1150. border-radius: 4px;
  1151. background: #fff;
  1152. box-sizing: border-box;
  1153. &::before {
  1154. content: '';
  1155. position: absolute;
  1156. top: -7px;
  1157. left: var(--arrow-left, 50%);
  1158. transform: translateX(-50%);
  1159. width: 0;
  1160. height: 0;
  1161. border-left: 8px solid transparent;
  1162. border-right: 8px solid transparent;
  1163. border-bottom: 8px solid #fff;
  1164. z-index: 3;
  1165. }
  1166. }
  1167. &__categories {
  1168. display: flex;
  1169. justify-content: space-between;
  1170. gap: 2px;
  1171. margin-bottom: 10px;
  1172. overflow-x: auto;
  1173. -webkit-overflow-scrolling: touch;
  1174. }
  1175. &__category {
  1176. position: relative;
  1177. flex: 1;
  1178. padding-bottom: 5px;
  1179. font-size: 13px;
  1180. color: #909090;
  1181. text-align: center;
  1182. &--active {
  1183. color: #333;
  1184. border-bottom: 2px solid #3A3A3A;
  1185. }
  1186. }
  1187. &__details {
  1188. display: grid;
  1189. grid-template-columns: repeat(4, 1fr);
  1190. gap: 10px;
  1191. padding-top: 6px;
  1192. }
  1193. &__detail {
  1194. position: relative;
  1195. display: flex;
  1196. align-items: center;
  1197. justify-content: center;
  1198. padding: 5px 6px;
  1199. border: 1px solid transparent;
  1200. border-radius: 4px;
  1201. background: #f5f5f5;
  1202. color: #909090;
  1203. font-size: 12px;
  1204. text-align: center;
  1205. box-sizing: border-box;
  1206. &--active {
  1207. border-color: #2199f8;
  1208. background: rgba(33, 153, 248, 0.1);
  1209. color: #2199f8;
  1210. }
  1211. }
  1212. &__detail-text {
  1213. display: block;
  1214. width: 100%;
  1215. overflow: hidden;
  1216. text-overflow: ellipsis;
  1217. white-space: nowrap;
  1218. }
  1219. &__badge {
  1220. position: absolute;
  1221. top: -9px;
  1222. right: -8px;
  1223. padding: 0 5px;
  1224. border-radius: 2px;
  1225. font-size: 10px;
  1226. color: #fff;
  1227. z-index: 1;
  1228. &--1 {
  1229. background: rgba(33, 153, 248, 0.35);
  1230. }
  1231. &--2 {
  1232. background: rgba(33, 153, 248, 0.65);
  1233. }
  1234. &--3 {
  1235. background: #2199f8;
  1236. }
  1237. }
  1238. }
  1239. .phenology-track-section {
  1240. margin-top: 15px;
  1241. display: flex;
  1242. flex-direction: column;
  1243. gap: 16px;
  1244. }
  1245. }
  1246. }
  1247. .phenology-tip-banner {
  1248. display: flex;
  1249. align-items: center;
  1250. justify-content: space-between;
  1251. gap: 8px;
  1252. padding: 10px;
  1253. border-radius: 8px;
  1254. background: rgba(255, 149, 61, 0.1);
  1255. border: 1px solid rgba(255, 149, 61, 0.5);
  1256. margin-top: 10px;
  1257. .banner__left {
  1258. flex: 1;
  1259. .banner__title {
  1260. font-size: 16px;
  1261. font-weight: 600;
  1262. color: #FF953D;
  1263. }
  1264. .banner__desc {
  1265. font-size: 12px;
  1266. color: rgba(0, 0, 0, 0.4);
  1267. }
  1268. }
  1269. .banner__btn {
  1270. display: flex;
  1271. align-items: center;
  1272. gap: 4px;
  1273. padding: 7px 16px;
  1274. border-radius: 25px;
  1275. color: #ffffff;
  1276. background: linear-gradient(180deg, #FFB273 0%, #FF953D 100%);
  1277. }
  1278. &.blue {
  1279. background: #F1F8FE;
  1280. border: 1px solid rgba(33, 153, 248, 0.5);
  1281. .banner__title {
  1282. color: #2199F8;
  1283. }
  1284. .banner__btn {
  1285. background: linear-gradient(180deg, #8ACBFF 0%, #2199F8 100%);
  1286. }
  1287. }
  1288. }
  1289. }
  1290. // .my-map {
  1291. // width: 100%;
  1292. // .map-container {
  1293. // width: 100%;
  1294. // height: 170px;
  1295. // margin: 10px 0;
  1296. // clip-path: inset(0px round 5px);
  1297. // position: relative;
  1298. // .tip {
  1299. // position: absolute;
  1300. // top: 0;
  1301. // left: 0;
  1302. // color: #fff;
  1303. // padding: 5px 10px;
  1304. // border-radius: 5px;
  1305. // background: rgba(0, 0, 0, 0.6);
  1306. // z-index: 2;
  1307. // }
  1308. // }
  1309. // }
  1310. .map-container {
  1311. width: 100%;
  1312. height: 170px;
  1313. margin: 10px 0;
  1314. clip-path: inset(0px round 5px);
  1315. position: relative;
  1316. .tip {
  1317. position: absolute;
  1318. top: 0;
  1319. left: 0;
  1320. color: #fff;
  1321. padding: 5px 10px;
  1322. border-radius: 5px;
  1323. background: rgba(0, 0, 0, 0.6);
  1324. z-index: 2;
  1325. }
  1326. }
  1327. .upload-progress-title {
  1328. color: rgba(60, 60, 60, 0.5);
  1329. font-weight: 500;
  1330. background: rgba(33, 153, 248, 0.1);
  1331. border-radius: 5px;
  1332. padding: 5px 10px;
  1333. margin-bottom: 12px;
  1334. .value {
  1335. color: #2199F8;
  1336. }
  1337. }
  1338. .upload-form {
  1339. .form-item {
  1340. margin-bottom: 10px;
  1341. .item-label {
  1342. font-size: 16px;
  1343. margin-bottom: 8px;
  1344. .optional {
  1345. font-size: 14px;
  1346. color: rgba(0, 0, 0, 0.2);
  1347. }
  1348. }
  1349. }
  1350. .growth-abnormal-type-field {
  1351. margin-bottom: 12px;
  1352. &__label {
  1353. font-size: 16px;
  1354. color: #000;
  1355. margin-bottom: 10px;
  1356. }
  1357. }
  1358. .growth-abnormal-type-select {
  1359. width: 100%;
  1360. :deep(.el-select__wrapper) {
  1361. min-height: 44px;
  1362. padding: 4px 12px;
  1363. border-radius: 6px;
  1364. border: 1px solid #d6d6d6;
  1365. box-shadow: none;
  1366. background: #fff;
  1367. }
  1368. }
  1369. .special-input {
  1370. padding: 8px;
  1371. background: rgba(33, 153, 248, 0.1);
  1372. border-radius: 5px;
  1373. border: 1px solid #2199F8;
  1374. }
  1375. }
  1376. </style>