fileFloat.vue 12 KB

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