fileFloat.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <template>
  2. <!-- <div class="add-btn">{{ t('点击新建管理分区') }}</div> -->
  3. <floating-panel class="file-float-panel" :class="{ 'custom-panel': height === anchors[0] }" v-model:height="height"
  4. :anchors="anchors">
  5. <div class="file-float-content">
  6. <div class="float-tabs">
  7. <div class="tab-active-bg" :style="primaryActiveBgStyle"></div>
  8. <div v-for="(item, index) in floatTabLabels" :key="item.value" class="tab-item"
  9. @click="changePrimaryTab(index)" :class="{ 'tab-item-active': activeTab === index }">
  10. {{ item.title }}
  11. </div>
  12. </div>
  13. <div class="tab-content-group" v-show="height !== anchors[0]">
  14. <template v-if="isAgriRecordTab">
  15. <div class="float-sub-tabs">
  16. <div v-for="(item, index) in agriSubTabLabels" :key="item.value" class="sub-tab-item"
  17. :class="{ 'sub-tab-item-active': activeSubTab === index }" @click="changeSubTab(index)">
  18. {{ item.title }}
  19. </div>
  20. </div>
  21. <div class="tab-loading" v-if="loading">{{ t('agriFile.loading') }}</div>
  22. <div class="tab-empty" v-else-if="currentList.length === 0">{{ t('agriFile.noData') }}</div>
  23. <div v-else class="tab-content-item" v-for="item in displayList" :key="item.id">
  24. <template v-if="item?.recordText?.length">
  25. <div class="time-tag">{{ item.time }}</div>
  26. <div class="item-info">
  27. {{ item.recordText }}
  28. <span class="blue-text">{{ item.ratio }}{{ item.showRatio ? '%' : '' }}</span>
  29. </div>
  30. </template>
  31. </div>
  32. </template>
  33. <div v-else-if="isRemoteSensingTab" class="remote-sensing-chart">
  34. <div class="remote-sensing-chart__header">
  35. <span class="remote-sensing-chart__title">{{ t('agriFile.remoteSensingChartTitle') }}</span>
  36. <div class="remote-sensing-chart__legend">
  37. <div
  38. v-for="item in remoteSensingLegendItems"
  39. :key="item.key"
  40. class="remote-sensing-chart__legend-item"
  41. >
  42. <span
  43. class="remote-sensing-chart__legend-line"
  44. :style="{ background: item.color }"
  45. ></span>
  46. <span class="remote-sensing-chart__legend-text">{{ item.label }}</span>
  47. </div>
  48. </div>
  49. </div>
  50. <div class="tab-loading" v-if="loading">{{ t('agriFile.loading') }}</div>
  51. <remote-sensing-chart
  52. v-else-if="hasRemoteSensingData"
  53. :chart-data="remoteSensingChartData"
  54. />
  55. <div class="tab-empty" v-else>{{ t('agriFile.noData') }}</div>
  56. </div>
  57. </div>
  58. </div>
  59. </floating-panel>
  60. </template>
  61. <script setup>
  62. import { useI18n } from "@/i18n";
  63. import { RECORD_KEY_MAP } from "@/i18n/recordTextMap";
  64. import { FloatingPanel } from 'vant';
  65. import { computed, ref, watch } from 'vue';
  66. import remoteSensingChart from './remoteSensingChart.vue';
  67. const { t } = useI18n();
  68. const props = defineProps({
  69. farmRecordData: {
  70. type: Object,
  71. default: () => ({}),
  72. },
  73. activeTab: {
  74. type: Number,
  75. default: 0,
  76. },
  77. activeSubTab: {
  78. type: Number,
  79. default: 0,
  80. },
  81. loading: {
  82. type: Boolean,
  83. default: false,
  84. },
  85. remoteSensingData: {
  86. type: Array,
  87. default: () => [],
  88. },
  89. });
  90. const emit = defineEmits(["update:activeTab", "update:activeSubTab"]);
  91. const anchors = [
  92. 130,
  93. Math.round(0.4 * window.innerHeight),
  94. Math.round(0.8 * window.innerHeight),
  95. ];
  96. const height = ref(anchors[0]);
  97. const AGRI_SUB_TAB_KEYS = ["phenology", "farming", "abnormal"];
  98. const floatTabLabels = computed(() => [
  99. { title: t("agriFile.tabAgriRecord"), value: "agriRecord" },
  100. { title: t("agriFile.tabRemoteSensing"), value: "remoteSensing" },
  101. ]);
  102. const agriSubTabLabels = computed(() => [
  103. { title: t("agriFile.tabPhenology"), value: "phenology" },
  104. { title: t("agriFile.tabFarming"), value: "farming" },
  105. { title: t("agriFile.tabAbnormal"), value: "abnormal" },
  106. ]);
  107. const currentList = ref([]);
  108. const isAgriRecordTab = computed(() => floatTabLabels.value[props.activeTab]?.value === "agriRecord");
  109. const isRemoteSensingTab = computed(() => floatTabLabels.value[props.activeTab]?.value === "remoteSensing");
  110. const REMOTE_SENSING_LEGEND_CONFIG = [
  111. { key: "zone", labelKey: "agriFile.remoteSensingLegendZone", color: "#2199F8" },
  112. { key: "standard", labelKey: "agriFile.remoteSensingLegendStandard", color: "#9FD1FA" },
  113. ];
  114. const remoteSensingLegendItems = computed(() =>
  115. REMOTE_SENSING_LEGEND_CONFIG.map(({ key, labelKey, color }) => ({
  116. key,
  117. label: t(labelKey),
  118. color,
  119. }))
  120. );
  121. const remoteSensingChartData = computed(() => {
  122. const raw = props.remoteSensingData;
  123. if (!raw) return null;
  124. if (!Array.isArray(raw) && raw.zoneSeries?.length) {
  125. return raw;
  126. }
  127. if (!Array.isArray(raw) || raw.length === 0) {
  128. return null;
  129. }
  130. const highlightIndex = raw.findIndex((item) => item.highlight);
  131. return {
  132. timeLabels: raw.map((item) => item.time ?? item.label ?? t("agriFile.timeAxisLabel")),
  133. zoneSeries: raw.map((item) => Number(item.zone ?? item.zoneValue ?? item.value)),
  134. standardSeries: raw.map((item) => Number(item.standard ?? item.standardValue ?? item.stdValue)),
  135. highlightIndex: highlightIndex >= 0 ? highlightIndex : undefined,
  136. };
  137. });
  138. const hasRemoteSensingData = computed(() => {
  139. const data = remoteSensingChartData.value;
  140. return Boolean(data?.zoneSeries?.length && data?.standardSeries?.length);
  141. });
  142. const activeSubTabValue = computed(
  143. () => agriSubTabLabels.value[props.activeSubTab]?.value || AGRI_SUB_TAB_KEYS[0]
  144. );
  145. const displayList = computed(() =>
  146. currentList.value.map((item) => ({
  147. ...item,
  148. recordText: t(RECORD_KEY_MAP[item.record] || item.record),
  149. showRatio: activeSubTabValue.value !== "farming" && String(item.ratio ?? "").length > 0,
  150. }))
  151. );
  152. const syncCurrentList = () => {
  153. if (!isAgriRecordTab.value) {
  154. currentList.value = [];
  155. return;
  156. }
  157. currentList.value = props.farmRecordData?.[activeSubTabValue.value] || [];
  158. };
  159. const changePrimaryTab = (index) => {
  160. emit("update:activeTab", index);
  161. syncCurrentList();
  162. };
  163. const changeSubTab = (index) => {
  164. emit("update:activeSubTab", index);
  165. syncCurrentList();
  166. };
  167. const primaryActiveBgStyle = computed(() => ({
  168. transform: `translateX(${props.activeTab * 100}%)`,
  169. }));
  170. watch(
  171. () => props.farmRecordData,
  172. () => {
  173. syncCurrentList();
  174. },
  175. { deep: true, immediate: true }
  176. );
  177. watch(() => props.activeTab, () => {
  178. syncCurrentList();
  179. });
  180. watch(() => props.activeSubTab, () => {
  181. syncCurrentList();
  182. });
  183. </script>
  184. <style lang="scss" scoped>
  185. .add-btn {
  186. position: fixed;
  187. top: 50%;
  188. left: 50%;
  189. transform: translate(-50%, -50%);
  190. color: #fff;
  191. border-radius: 20px;
  192. padding: 0 20px;
  193. background: #2199f8;
  194. height: 40px;
  195. line-height: 40px;
  196. cursor: pointer;
  197. }
  198. .file-float-panel {
  199. left: 12px;
  200. width: calc(100% - 24px);
  201. &.custom-panel {
  202. background: transparent;
  203. ::v-deep {
  204. .van-floating-panel__header {
  205. background: #fff;
  206. border-radius: 10px 10px 0 0;
  207. }
  208. .van-floating-panel__content {
  209. background: transparent;
  210. margin-top: -1px;
  211. }
  212. }
  213. }
  214. }
  215. .file-float-content {
  216. padding: 0 10px 10px;
  217. background: #fff;
  218. border-radius: 0 0 10px 10px;
  219. .float-tabs {
  220. position: relative;
  221. border-radius: 4px;
  222. padding: 3px;
  223. background: #E9E9E9;
  224. display: grid;
  225. grid-template-columns: repeat(2, minmax(0, 1fr));
  226. align-items: center;
  227. overflow: hidden;
  228. .tab-active-bg {
  229. position: absolute;
  230. top: 3px;
  231. left: 3px;
  232. width: calc((100% - 6px) / 2);
  233. height: 26px;
  234. border-radius: 4px;
  235. background: #fff;
  236. transition: transform 0.25s ease;
  237. }
  238. .tab-item {
  239. position: relative;
  240. z-index: 1;
  241. flex: 1;
  242. height: 26px;
  243. line-height: 26px;
  244. text-align: center;
  245. color: #767676;
  246. border-radius: 4px;
  247. transition: color 0.2s ease;
  248. &.tab-item-active {
  249. color: #0D0D0D;
  250. }
  251. }
  252. }
  253. .float-sub-tabs {
  254. display: flex;
  255. align-items: center;
  256. gap: 8px;
  257. margin-bottom: 10px;
  258. .sub-tab-item {
  259. height: 26px;
  260. line-height: 24px;
  261. padding: 0 10px;
  262. font-size: 12px;
  263. color: #767676;
  264. background: #e9e9e9;
  265. border-radius: 4px;
  266. border: 1px solid transparent;
  267. box-sizing: border-box;
  268. cursor: pointer;
  269. transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
  270. &.sub-tab-item-active {
  271. color: #2199f8;
  272. background: #fff;
  273. border-color: #2199f8;
  274. }
  275. }
  276. }
  277. .tab-content-group {
  278. padding-top: 12px;
  279. .tab-loading,
  280. .tab-empty {
  281. text-align: center;
  282. color: #9a9a9a;
  283. font-size: 13px;
  284. padding: 14px 0;
  285. }
  286. .tab-content-item+.tab-content-item {
  287. margin-top: 10px;
  288. }
  289. .tab-content-item {
  290. display: flex;
  291. align-items: center;
  292. gap: 10px;
  293. .time-tag {
  294. color: #2199F8;
  295. background: rgba(33, 153, 248, 0.1);
  296. font-size: 12px;
  297. height: 21px;
  298. line-height: 21px;
  299. padding: 0 6px;
  300. min-width: fit-content;
  301. box-sizing: border-box;
  302. }
  303. .item-info {
  304. color: rgba(60, 60, 60, 0.5);
  305. line-height: 21px;
  306. }
  307. .blue-text {
  308. color: #2199f8;
  309. }
  310. }
  311. .remote-sensing-chart {
  312. &__header {
  313. display: flex;
  314. align-items: center;
  315. justify-content: space-between;
  316. gap: 12px;
  317. }
  318. &__legend {
  319. display: flex;
  320. align-items: center;
  321. justify-content: flex-end;
  322. flex-wrap: wrap;
  323. gap: 12px;
  324. }
  325. &__legend-item {
  326. display: flex;
  327. align-items: center;
  328. gap: 6px;
  329. }
  330. &__legend-line {
  331. width: 14px;
  332. height: 4px;
  333. border-radius: 2px;
  334. }
  335. &__legend-text {
  336. font-size: 12px;
  337. color: #666666;
  338. line-height: 18px;
  339. }
  340. }
  341. }
  342. }
  343. </style>