diagnosisReport.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. <template>
  2. <div
  3. class="diagnosis-report-page"
  4. v-loading="pageLoading"
  5. :element-loading-text="loadingText"
  6. >
  7. <div class="diagnosis-header">
  8. <div class="diagnosis-header__title-row">
  9. <span class="diagnosis-header__title">种植报告</span>
  10. <img class="diagnosis-header__icon" src="@/assets/img/report/star.png" alt="" />
  11. </div>
  12. <div class="diagnosis-header__desc">{{ headerDesc }}</div>
  13. <div class="diagnosis-header__base">{{ headerBase }}</div>
  14. </div>
  15. <div class="diagnosis-content">
  16. <div class="variety-bar">
  17. <div class="variety-bar__tags">
  18. <div v-for="item in varietyList" :key="item.id" class="variety-bar__tag"
  19. :class="{ active: item.id === activeVarietyId }" @click="activeVarietyId = item.id">
  20. {{ item.name }}
  21. </div>
  22. </div>
  23. <div class="variety-bar__add">
  24. <el-icon>
  25. <Plus />
  26. </el-icon>
  27. <span>新增品种</span>
  28. </div>
  29. </div>
  30. <div class="base-data">
  31. <div class="base-data__title">
  32. <img class="base-data__icon" src="@/assets/img/report/base-icon.png" alt="" />
  33. <span>基本信息</span>
  34. </div>
  35. <div class="base-data__body">
  36. <div v-if="!loading && !baseInfoList.length" class="base-data__empty">暂无基本信息</div>
  37. <div v-for="item in baseInfoList" :key="item.key" class="base-info-item">
  38. <div class="base-info-item__row">
  39. <span class="base-info-item__label">{{ item.label }}</span>
  40. <span class="base-info-item__value">{{ item.value }}</span>
  41. <span v-if="item.actionInline" class="base-info-action"
  42. @click="handleBaseAction(item.actionInline.key)">
  43. {{ item.actionInline.text }}
  44. <el-icon><Edit /></el-icon>
  45. </span>
  46. </div>
  47. <div v-if="item.actionBlock" class="base-info-action base-info-action--block"
  48. @click="handleBaseAction(item.actionBlock.key)">
  49. {{ item.actionBlock.text }}
  50. <el-icon><Edit /></el-icon>
  51. </div>
  52. </div>
  53. </div>
  54. </div>
  55. <div class="report-module">
  56. <div class="report-tabs">
  57. <div
  58. v-for="tab in reportTabs"
  59. :key="tab.key"
  60. class="report-tabs__item"
  61. :class="{ active: tab.key === activeReportTab }"
  62. @click="activeReportTab = tab.key"
  63. >
  64. <img class="report-tabs__icon" :src="tab.icon" alt="" />
  65. <span class="report-tabs__label">{{ tab.label }}</span>
  66. </div>
  67. </div>
  68. <div class="report-panel">
  69. <div class="report-panel__title">{{ activeReportTabMeta.label }}</div>
  70. <div
  71. v-if="activeReportHtml"
  72. class="report-panel__body"
  73. v-html="activeReportHtml"
  74. ></div>
  75. <div v-else class="report-panel__empty">暂无报告内容</div>
  76. </div>
  77. </div>
  78. </div>
  79. <adjustPhenologyPopup ref="adjustPhenologyPopupRef" @confirm="handlePhenologyConfirm" />
  80. <restingDatePopup
  81. ref="restingDatePopupRef"
  82. @confirm="handleRestingDateConfirm"
  83. @switchGrowing="handleSwitchGrowing"
  84. />
  85. <editPlantPointPopup ref="editPlantPointPopupRef" @confirm="handlePlantPointConfirm" />
  86. <!-- 暂无农场时不弹「完善农场信息」,逻辑保留备用
  87. <complete-farm-info-popup
  88. v-model:show="showCompleteFarmPopup"
  89. :image="completeFarmPopupImage"
  90. highlight-text="种植全析报告"
  91. @confirm="goCompleteFarmInfo"
  92. />
  93. -->
  94. <restore-planting-popup />
  95. </div>
  96. </template>
  97. <script setup>
  98. import { computed, onActivated, onDeactivated, onBeforeUnmount, ref } from "vue";
  99. import { useRouter } from "vue-router";
  100. // import { useStore } from "vuex";
  101. import { ElMessage } from "element-plus";
  102. import { Plus, Edit } from "@element-plus/icons-vue";
  103. import adjustPhenologyPopup from "../components/adjustPhenologyPopup.vue";
  104. import restingDatePopup from "../components/restingDatePopup.vue";
  105. import editPlantPointPopup from "../components/editPlantPointPopup.vue";
  106. // import completeFarmInfoPopup from "@/components/popup/completeFarmInfoPopup.vue";
  107. import tab1Icon from "@/assets/img/report/tab-1.png";
  108. import tab2Icon from "@/assets/img/report/tab-2.png";
  109. import tab3Icon from "@/assets/img/report/tab-3.png";
  110. import tab4Icon from "@/assets/img/report/tab-4.png";
  111. import tab5Icon from "@/assets/img/report/tab-5.png";
  112. // import completeFarmPopupImage from "@/assets/img/common/empty-bg-3.png";
  113. import restorePlantingPopup from "@/components/popup/restorePlantingPopup.vue";
  114. // ---------------------------------------------------------------------------
  115. // Props / Emits — 无
  116. // ---------------------------------------------------------------------------
  117. const router = useRouter();
  118. // const store = useStore();
  119. // ---------------------------------------------------------------------------
  120. // 常量
  121. // ---------------------------------------------------------------------------
  122. /** 报告未生成时轮询间隔(ms) */
  123. const REPORT_POLL_INTERVAL = 6000;
  124. /** 无农场时默认作物编码:水稻 */
  125. const NO_FARM_CROP = "PAD";
  126. /** 无定位时的默认点位(与录入页一致) */
  127. const DEFAULT_POINT = "POINT(113.6142086995688 23.585836479509055)";
  128. /** module1_basic 字段标签 → 基本信息展示项(labels 含接口原文别名) */
  129. const BASE_FIELD_META = [
  130. { key: "crop", labels: ["作物品种", "种植作物"], label: "种植作物:", actionInline: { key: "changeVariety", text: "更改品种" } },
  131. { key: "trait", labels: ["品种特质", "品种优势"], label: "品种特质:" },
  132. { key: "phenology", labels: ["物候信息", "物候期"], label: "物候信息:", actionInline: { key: "adjustPhenology", text: "调整物候" } },
  133. { key: "soil", labels: ["土壤条件"], label: "土壤条件:", actionBlock: { key: "soilSense", text: "土壤感知" } },
  134. { key: "terrain", labels: ["地形条件"], label: "地形条件:", actionInline: { key: "editLocation", text: "修改定位" } },
  135. ];
  136. /** Tab key → report/check 返回字段 */
  137. const REPORT_TAB_FIELD = {
  138. weather: "module2_weather",
  139. pest: "module3_pest",
  140. quality: "module4_quality",
  141. input: "module5_input",
  142. manage: "module6_management",
  143. };
  144. const reportTabs = [
  145. { key: "weather", label: "气象风险", icon: tab1Icon },
  146. { key: "pest", label: "病虫风险", icon: tab2Icon },
  147. { key: "quality", label: "品质潜力", icon: tab3Icon },
  148. { key: "input", label: "必要投入", icon: tab4Icon },
  149. { key: "manage", label: "农事管理", icon: tab5Icon },
  150. ];
  151. // ---------------------------------------------------------------------------
  152. // 页面状态
  153. // ---------------------------------------------------------------------------
  154. const loading = ref(false); // 首次拉取中
  155. const reportGenerating = ref(false); // 报告尚未生成,持续 loading + 轮询
  156. const varietyList = ref([]);
  157. const activeVarietyId = ref(null);
  158. const headerDesc = ref("");
  159. const headerBase = ref("");
  160. const baseInfoList = ref([]);
  161. /** 各 Tab 对应的 markdown 原文 */
  162. const moduleMarkdownMap = ref({});
  163. const activeReportTab = ref("weather");
  164. const adjustPhenologyPopupRef = ref(null);
  165. const restingDatePopupRef = ref(null);
  166. const editPlantPointPopupRef = ref(null);
  167. // const showCompleteFarmPopup = ref(false); // 无农场时展示「完善农场信息」
  168. let pollTimer = null;
  169. /** 当前轮询方式:farm | latlon */
  170. let pollMode = null;
  171. let pollParams = null;
  172. // ---------------------------------------------------------------------------
  173. // 计算属性
  174. // ---------------------------------------------------------------------------
  175. const pageLoading = computed(() => loading.value || reportGenerating.value);
  176. const loadingText = "报告生成中,请稍后...";
  177. const activeReportTabMeta = computed(
  178. () => reportTabs.find((tab) => tab.key === activeReportTab.value) || reportTabs[0]
  179. );
  180. const activeReportHtml = computed(() => {
  181. const field = REPORT_TAB_FIELD[activeReportTab.value];
  182. const md = moduleMarkdownMap.value[field] || "";
  183. return md ? markdownToHtml(md) : "";
  184. });
  185. // ---------------------------------------------------------------------------
  186. // 工具函数
  187. // ---------------------------------------------------------------------------
  188. // function getMiniUserId() {
  189. // return store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID");
  190. // }
  191. function getSelectedFarm() {
  192. try {
  193. return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
  194. } catch {
  195. return {};
  196. }
  197. }
  198. /** 与 diagnosisReportPopup 同源:标记该农场/分区弹窗已读 */
  199. const DIAGNOSIS_POPUP_SHOWN_PREFIX = "DIAGNOSIS_REPORT_POPUP_SHOWN_";
  200. function markDiagnosisPopupShown(farmId, zoneId) {
  201. if (farmId == null || farmId === "") return;
  202. const key = `${DIAGNOSIS_POPUP_SHOWN_PREFIX}${farmId}_${zoneId ?? ""}`;
  203. localStorage.setItem(key, "1");
  204. }
  205. function hasFarmZone(farm = {}) {
  206. const farmId = farm.farm_id ?? farm.id;
  207. const zoneId = farm.zone_id ?? farm.zoneId;
  208. return farmId != null && farmId !== "" && zoneId != null && zoneId !== "";
  209. }
  210. /** 无农场报告用点位:优先用户定位 */
  211. function getPointText() {
  212. return localStorage.getItem("MINI_USER_LOCATION_POINT") || DEFAULT_POINT;
  213. }
  214. /** report6_latlon:有 records[0] 即视为报告已生成 */
  215. function pickLatlonReport(res) {
  216. const records = res?.data?.records;
  217. if (Array.isArray(records) && records.length) return records[0];
  218. return null;
  219. }
  220. /** 解析 module1_basic:兼容 `**标签**:` 与纯文本 `标签:`(内容可跨行) */
  221. function parseBasicSections(markdown = "") {
  222. const text = String(markdown).replace(/\r\n/g, "\n").trim();
  223. if (!text) return {};
  224. const collect = (re) => {
  225. const matches = [...text.matchAll(re)];
  226. if (!matches.length) return null;
  227. const sections = {};
  228. matches.forEach((match, index) => {
  229. const label = String(match[1] || "").trim();
  230. const start = match.index + match[0].length;
  231. const end = index + 1 < matches.length ? matches[index + 1].index : text.length;
  232. if (label) sections[label] = text.slice(start, end).trim();
  233. });
  234. return sections;
  235. };
  236. // 1) markdown:**作物品种**:xxx
  237. const mdSections = collect(/\*\*([^*]+)\*\*[::]\s*/g);
  238. if (mdSections) return mdSections;
  239. // 2) 纯文本:作物品种:xxx(按已知标签切分,避免正文里的冒号误伤)
  240. const knownLabels = [...new Set(BASE_FIELD_META.flatMap((meta) => meta.labels))];
  241. const escaped = knownLabels
  242. .map((label) => label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
  243. .join("|");
  244. if (!escaped) return {};
  245. return collect(new RegExp(`(?:^|\\n)(${escaped})[::]\\s*`, "g")) || {};
  246. }
  247. function findSectionValue(sections, labels) {
  248. for (const label of labels) {
  249. if (sections[label]) return sections[label];
  250. }
  251. return "";
  252. }
  253. function buildBaseInfoList(sections, report = {}) {
  254. return BASE_FIELD_META.map((meta) => {
  255. let value = findSectionValue(sections, meta.labels);
  256. // 种植作物兜底:接口顶层 crop / variety
  257. if (!value && meta.key === "crop") {
  258. const crop = report.crop || "";
  259. const variety = report.variety || "";
  260. value = [crop, variety].filter(Boolean).join("-");
  261. }
  262. return {
  263. key: meta.key,
  264. label: meta.label,
  265. value: value || "—",
  266. actionInline: meta.actionInline,
  267. actionBlock: meta.actionBlock,
  268. };
  269. });
  270. }
  271. /** 轻量 markdown → HTML(标题 / 粗体 / 列表) */
  272. function markdownToHtml(markdown = "") {
  273. const escapeHtml = (str) =>
  274. String(str)
  275. .replace(/&/g, "&amp;")
  276. .replace(/</g, "&lt;")
  277. .replace(/>/g, "&gt;");
  278. const lines = String(markdown).replace(/\r\n/g, "\n").split("\n");
  279. const html = [];
  280. let inList = false;
  281. const flushList = () => {
  282. if (!inList) return;
  283. html.push("</ul>");
  284. inList = false;
  285. };
  286. const inline = (str) =>
  287. escapeHtml(str).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
  288. lines.forEach((raw) => {
  289. const line = raw.trimEnd();
  290. const trimmed = line.trim();
  291. if (!trimmed) {
  292. flushList();
  293. return;
  294. }
  295. if (/^###\s+/.test(trimmed)) {
  296. flushList();
  297. html.push(`<h4>${inline(trimmed.replace(/^###\s+/, ""))}</h4>`);
  298. return;
  299. }
  300. if (/^##\s+/.test(trimmed)) {
  301. flushList();
  302. html.push(`<h3>${inline(trimmed.replace(/^##\s+/, ""))}</h3>`);
  303. return;
  304. }
  305. if (/^#\s+/.test(trimmed)) {
  306. flushList();
  307. html.push(`<h2>${inline(trimmed.replace(/^#\s+/, ""))}</h2>`);
  308. return;
  309. }
  310. if (/^[-*]\s+/.test(trimmed)) {
  311. if (!inList) {
  312. html.push("<ul>");
  313. inList = true;
  314. }
  315. html.push(`<li>${inline(trimmed.replace(/^[-*]\s+/, ""))}</li>`);
  316. return;
  317. }
  318. flushList();
  319. html.push(`<p>${inline(trimmed)}</p>`);
  320. });
  321. flushList();
  322. return html.join("");
  323. }
  324. function applyReportData(report, farm) {
  325. const region = report.region || farm.rawAddress || farm.farm_address || farm.city || "";
  326. const farmName = farm.farm_name || farm.name || "";
  327. headerDesc.value = region ? `${region}的飞鸟智慧农业` : "飞鸟智慧农业";
  328. headerBase.value = farmName || "我的基地";
  329. const cropName = report.variety || report.crop || farm.variety_name || farm.categoryLabel || "作物";
  330. varietyList.value = [{ id: report.id || 1, name: cropName }];
  331. activeVarietyId.value = varietyList.value[0].id;
  332. const sections = parseBasicSections(report.module1_basic || "");
  333. baseInfoList.value = buildBaseInfoList(sections, report);
  334. moduleMarkdownMap.value = {
  335. module2_weather: report.module2_weather || "",
  336. module3_pest: report.module3_pest || "",
  337. module4_quality: report.module4_quality || "",
  338. module5_input: report.module5_input || "",
  339. module6_management: report.module6_management || "",
  340. };
  341. }
  342. function resetReportView(farm = {}) {
  343. headerDesc.value = farm.rawAddress || farm.farm_address || farm.city
  344. ? `${farm.rawAddress || farm.farm_address || farm.city}的飞鸟智慧农业`
  345. : "飞鸟智慧农业";
  346. headerBase.value = farm.farm_name || farm.name || "我的基地";
  347. varietyList.value = [];
  348. activeVarietyId.value = null;
  349. baseInfoList.value = [];
  350. moduleMarkdownMap.value = {};
  351. }
  352. // ---------------------------------------------------------------------------
  353. // 业务逻辑:无农场弹窗 / 加载报告
  354. // ---------------------------------------------------------------------------
  355. /** 无果园信息时展示「完善农场信息」弹窗(暂不启用) */
  356. // async function checkCompleteFarmPopup() {
  357. // const id = getMiniUserId();
  358. // if (!id) {
  359. // showCompleteFarmPopup.value = false;
  360. // return;
  361. // }
  362. // try {
  363. // const res = await VE_API.questionnaire.getFarms({ id });
  364. // const list = Array.isArray(res?.data) ? res.data : [];
  365. // showCompleteFarmPopup.value = list.length === 0;
  366. // } catch (e) {
  367. // console.warn("[diagnosisReport] getFarms failed", e);
  368. // showCompleteFarmPopup.value = false;
  369. // }
  370. // }
  371. function stopReportPolling() {
  372. if (pollTimer) {
  373. clearInterval(pollTimer);
  374. pollTimer = null;
  375. }
  376. pollMode = null;
  377. pollParams = null;
  378. }
  379. /** 报告未就绪时保持 loading,并定时重试 */
  380. function startReportPolling(mode, params) {
  381. stopReportPolling();
  382. pollMode = mode;
  383. pollParams = params;
  384. pollTimer = setInterval(() => {
  385. if (pollMode === "farm") {
  386. pollFarmReport(pollParams);
  387. } else if (pollMode === "latlon") {
  388. pollLatlonReport(pollParams);
  389. }
  390. }, REPORT_POLL_INTERVAL);
  391. }
  392. async function pollFarmReport({ farmId, zoneId }) {
  393. try {
  394. const raw = await VE_API.questionnaire.checkReportGenerated({
  395. farm_id: farmId,
  396. zone_id: zoneId,
  397. });
  398. const res = Array.isArray(raw) ? raw[0] : raw;
  399. const report = res?.data?.report;
  400. if (res?.code === 200 && report) {
  401. // 进入报告页并拿到数据:标记该农场弹窗已读,避免再弹
  402. markDiagnosisPopupShown(farmId, zoneId);
  403. stopReportPolling();
  404. reportGenerating.value = false;
  405. applyReportData(report, getSelectedFarm());
  406. }
  407. } catch (error) {
  408. console.warn("[diagnosisReport] farm poll failed", error);
  409. }
  410. }
  411. async function pollLatlonReport({ pointText, crop }) {
  412. try {
  413. const raw = await VE_API.report.report6Latlon({
  414. point_text: pointText,
  415. crop: '水稻',
  416. });
  417. const res = Array.isArray(raw) ? raw[0] : raw;
  418. const report = pickLatlonReport(res);
  419. if (res?.code === 200 && report) {
  420. stopReportPolling();
  421. reportGenerating.value = false;
  422. applyReportData(report, {});
  423. }
  424. } catch (error) {
  425. console.warn("[diagnosisReport] latlon poll failed", error);
  426. }
  427. }
  428. /** 无农场:按定位点 + 水稻拉取 / 轮询六模块报告 */
  429. async function fetchLatlonReport() {
  430. const pointText = getPointText();
  431. const crop = NO_FARM_CROP;
  432. const params = { pointText, crop };
  433. loading.value = true;
  434. try {
  435. const raw = await VE_API.report.report6Latlon({
  436. point_text: pointText,
  437. crop: '水稻',
  438. });
  439. const res = Array.isArray(raw) ? raw[0] : raw;
  440. const report = pickLatlonReport(res);
  441. if (res?.code === 200 && report) {
  442. applyReportData(report, {});
  443. return;
  444. }
  445. resetReportView({});
  446. reportGenerating.value = true;
  447. startReportPolling("latlon", params);
  448. } catch (error) {
  449. console.error("fetchLatlonReport failed", error);
  450. resetReportView({});
  451. reportGenerating.value = false;
  452. ElMessage.error("报告加载失败");
  453. } finally {
  454. loading.value = false;
  455. }
  456. }
  457. async function fetchFarmReport(farm) {
  458. const farmId = farm.farm_id ?? farm.id;
  459. const zoneId = farm.zone_id ?? farm.zoneId;
  460. loading.value = true;
  461. try {
  462. const raw = await VE_API.questionnaire.checkReportGenerated({
  463. farm_id: farmId,
  464. zone_id: zoneId,
  465. });
  466. const res = Array.isArray(raw) ? raw[0] : raw;
  467. const report = res?.data?.report;
  468. if (res?.code === 200 && report) {
  469. markDiagnosisPopupShown(farmId, zoneId);
  470. applyReportData(report, farm);
  471. return;
  472. }
  473. resetReportView(farm);
  474. reportGenerating.value = true;
  475. startReportPolling("farm", { farmId, zoneId });
  476. } catch (error) {
  477. console.error("fetchFarmReport failed", error);
  478. resetReportView(farm);
  479. reportGenerating.value = false;
  480. ElMessage.error("报告加载失败");
  481. } finally {
  482. loading.value = false;
  483. }
  484. }
  485. async function fetchDiagnosisReport() {
  486. const farm = getSelectedFarm();
  487. stopReportPolling();
  488. reportGenerating.value = false;
  489. if (!hasFarmZone(farm)) {
  490. await fetchLatlonReport();
  491. return;
  492. }
  493. await fetchFarmReport(farm);
  494. }
  495. // ---------------------------------------------------------------------------
  496. // 事件处理 / 生命周期
  497. // ---------------------------------------------------------------------------
  498. const handleBaseAction = (key) => {
  499. if (key === "adjustPhenology") {
  500. adjustPhenologyPopupRef.value?.open();
  501. return;
  502. }
  503. if (key === "editLocation") {
  504. editPlantPointPopupRef.value?.open();
  505. return;
  506. }
  507. if (key === "soilSense") {
  508. router.push("/soil_sense");
  509. return;
  510. }
  511. console.log("base action:", key);
  512. };
  513. const handlePhenologyConfirm = (payload) => {
  514. restingDatePopupRef.value?.open(payload);
  515. };
  516. const handleSwitchGrowing = () => {
  517. adjustPhenologyPopupRef.value?.open();
  518. };
  519. const handleRestingDateConfirm = (payload) => {
  520. console.log("resting date confirm:", payload);
  521. };
  522. const handlePlantPointConfirm = (payload) => {
  523. console.log("plant point confirm:", payload);
  524. };
  525. // const goCompleteFarmInfo = () => {
  526. // router.push("/entry_information");
  527. // };
  528. // tabbar 切换时组件被 keep-alive 缓存,需在激活时重新拉报告 / 校验农场
  529. onActivated(() => {
  530. // checkCompleteFarmPopup(); // 暂无农场时不弹「完善农场信息」
  531. fetchDiagnosisReport();
  532. });
  533. onDeactivated(() => {
  534. stopReportPolling();
  535. });
  536. onBeforeUnmount(() => {
  537. stopReportPolling();
  538. });
  539. </script>
  540. <style lang="scss" scoped>
  541. .diagnosis-report-page {
  542. width: 100%;
  543. height: calc(100vh - 50px);
  544. overflow: auto;
  545. -webkit-overflow-scrolling: touch;
  546. background: #f5f6fe;
  547. box-sizing: border-box;
  548. padding-bottom: 20px;
  549. .diagnosis-header {
  550. height: 148px;
  551. padding: 26px 0 0 14px;
  552. box-sizing: border-box;
  553. background: url("@/assets/img/report/title-bg.png") no-repeat center center / 100% 100%;
  554. color: #fff;
  555. &__title-row {
  556. display: flex;
  557. align-items: center;
  558. gap: 6px;
  559. }
  560. &__title {
  561. font-size: 35px;
  562. font-family: "pangmenzhengdao";
  563. }
  564. &__icon {
  565. width: 24px;
  566. height: 24px;
  567. }
  568. }
  569. .diagnosis-content {
  570. padding: 0 10px;
  571. .variety-bar {
  572. display: flex;
  573. align-items: center;
  574. justify-content: space-between;
  575. padding: 10px 6px;
  576. background: #fff;
  577. border-radius: 8px;
  578. margin-top: -20px;
  579. &__tags {
  580. display: flex;
  581. align-items: center;
  582. flex-wrap: wrap;
  583. gap: 6px;
  584. }
  585. &__tag {
  586. padding: 5px 16px;
  587. border-radius: 4px;
  588. background: #f2f3f5;
  589. color: #4e5969;
  590. &.active {
  591. background: #2199f8;
  592. color: #fff;
  593. }
  594. }
  595. &__add {
  596. display: flex;
  597. align-items: center;
  598. gap: 2px;
  599. padding: 5px 10px;
  600. border-radius: 4px;
  601. border: 1px solid #2199f8;
  602. background: rgba(33, 153, 248, 0.1);
  603. color: #2199f8;
  604. }
  605. }
  606. .base-data {
  607. margin: 10px 0;
  608. padding: 5px;
  609. // min-height: 382px;
  610. min-height: auto;
  611. background: url("@/assets/img/report/base-bg.png") no-repeat center top / 100% 100%;
  612. box-sizing: border-box;
  613. &__title {
  614. display: flex;
  615. align-items: center;
  616. gap: 5px;
  617. font-size: 17px;
  618. color: #fff;
  619. margin: -3px 0 0 -3px;
  620. }
  621. &__icon {
  622. width: 44px;
  623. height: 44px;
  624. }
  625. &__title>span {
  626. margin-top: -4px;
  627. }
  628. &__body {
  629. margin-top: -8px;
  630. padding: 10px;
  631. background: #fff;
  632. border-radius: 8px;
  633. .base-data__empty {
  634. padding: 20px 0;
  635. text-align: center;
  636. color: #999;
  637. font-size: 13px;
  638. }
  639. .base-info-item {
  640. margin-bottom: 8px;
  641. &__row {
  642. color: #333;
  643. }
  644. &__label {
  645. color: #999;
  646. }
  647. &__value {
  648. color: #333;
  649. }
  650. }
  651. .base-info-action {
  652. display: inline-flex;
  653. align-items: center;
  654. gap: 2px;
  655. margin-left: 6px;
  656. padding: 2px 8px;
  657. border-radius: 20px;
  658. background: rgba(33, 153, 248, 0.1);
  659. color: #2199f8;
  660. font-size: 12px;
  661. .el-icon {
  662. font-size: 12px;
  663. }
  664. &--block {
  665. display: inline-flex;
  666. margin: 8px 0 0;
  667. }
  668. }
  669. }
  670. }
  671. .report-module {
  672. margin-top: 4px;
  673. }
  674. .report-tabs {
  675. display: flex;
  676. align-items: stretch;
  677. gap: 8px;
  678. overflow-x: auto;
  679. -webkit-overflow-scrolling: touch;
  680. scrollbar-width: none;
  681. &::-webkit-scrollbar {
  682. display: none;
  683. }
  684. &__item {
  685. flex: 0 0 calc((100% - 24px) / 4);
  686. display: flex;
  687. flex-direction: column;
  688. align-items: center;
  689. justify-content: center;
  690. padding: 5px 5px 14px;
  691. box-sizing: border-box;
  692. color: #ADADAD;
  693. background: url("@/assets/img/report/tab-bg.png") no-repeat center center / 100% 100%;
  694. &.active {
  695. background: url("@/assets/img/report/tab-act-bg.png") no-repeat center center / 100% 100%;
  696. &::after {
  697. opacity: 1;
  698. }
  699. .report-tabs__icon {
  700. filter: none;
  701. opacity: 1;
  702. }
  703. }
  704. }
  705. &__icon {
  706. width: 22px;
  707. height: 22px;
  708. filter: grayscale(1);
  709. opacity: 0.45;
  710. }
  711. }
  712. .report-panel {
  713. margin-top: 10px;
  714. padding: 16px 14px 18px;
  715. background: #fff;
  716. border-radius: 10px;
  717. &__title {
  718. position: relative;
  719. display: inline-block;
  720. font-size: 18px;
  721. font-weight: 600;
  722. color: #1d2129;
  723. line-height: 1.3;
  724. z-index: 1;
  725. &::after {
  726. content: "";
  727. position: absolute;
  728. left: 0;
  729. bottom: 2px;
  730. width: 36px;
  731. height: 8px;
  732. border-radius: 8px;
  733. background: linear-gradient(90deg, #8ed0ff 0%, rgba(142, 208, 255, 0.35) 100%);
  734. z-index: -1;
  735. }
  736. }
  737. &__empty {
  738. margin-top: 14px;
  739. padding: 24px 0;
  740. text-align: center;
  741. color: #999;
  742. font-size: 13px;
  743. }
  744. &__body {
  745. margin-top: 12px;
  746. color: #333;
  747. font-size: 14px;
  748. line-height: 1.7;
  749. word-break: break-word;
  750. :deep(h2) {
  751. margin: 0 0 10px;
  752. font-size: 16px;
  753. font-weight: 600;
  754. color: #1d2129;
  755. }
  756. :deep(h3) {
  757. margin: 14px 0 8px;
  758. font-size: 15px;
  759. font-weight: 600;
  760. color: #1d2129;
  761. }
  762. :deep(h4) {
  763. margin: 12px 0 6px;
  764. font-size: 14px;
  765. font-weight: 600;
  766. color: #4e5969;
  767. }
  768. :deep(p) {
  769. margin: 0 0 8px;
  770. }
  771. :deep(ul) {
  772. margin: 0 0 10px;
  773. padding-left: 18px;
  774. }
  775. :deep(li) {
  776. margin-bottom: 4px;
  777. }
  778. :deep(strong) {
  779. font-weight: 600;
  780. color: #1d2129;
  781. }
  782. }
  783. }
  784. }
  785. }
  786. </style>