|
|
@@ -0,0 +1,429 @@
|
|
|
+<template>
|
|
|
+ <div class="region-albums">
|
|
|
+ <div class="region-albums__hero" :style="heroStyle">
|
|
|
+ <div class="region-albums__hero-mask"></div>
|
|
|
+ <div class="region-albums__nav" @click="goBack">
|
|
|
+ <el-icon class="region-albums__back-icon" color="#fff" :size="18">
|
|
|
+ <ArrowLeftBold />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="region-albums__hero-info">
|
|
|
+ <span class="region-albums__title">{{ regionName }}</span>
|
|
|
+ <span class="region-albums__count">{{ totalCount }}张</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="region-albums__panel" v-loading="loading">
|
|
|
+ <div v-if="!loading && !dateGroups.length" class="region-albums__empty">暂无相册照片</div>
|
|
|
+
|
|
|
+ <div class="region-albums__album-list">
|
|
|
+ <div
|
|
|
+ v-for="group in dateGroups"
|
|
|
+ :key="group.date"
|
|
|
+ class="album-group"
|
|
|
+ >
|
|
|
+ <div class="album-group__header">
|
|
|
+ <div class="album-group__date">{{ group.dateLabel }}</div>
|
|
|
+ <div class="album-group__remark">{{ group.remark || '备注备注备注备注' }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="album-group__grid">
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in group.list"
|
|
|
+ :key="item.id || `${group.date}-${index}`"
|
|
|
+ class="album-group__item"
|
|
|
+ @click="previewImage(group, index)"
|
|
|
+ >
|
|
|
+ <img class="album-group__img" :src="item.url" alt="" loading="lazy" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="region-albums__fab" @click="handleAdd">
|
|
|
+ <el-icon :size="28" color="#fff"><Plus /></el-icon>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <album-upload-popup
|
|
|
+ v-model:show="showUploadPopup"
|
|
|
+ @confirm="handleUploadConfirm"
|
|
|
+ />
|
|
|
+
|
|
|
+ <ImagePreviewPopup
|
|
|
+ v-model:show="showPreview"
|
|
|
+ :images="previewImages"
|
|
|
+ :start-index="previewIndex"
|
|
|
+ :show-remark="true"
|
|
|
+ @remark-change="handleRemarkChange"
|
|
|
+ @remark-confirm="handleRemarkConfirm"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, onMounted, ref } from "vue";
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import { ArrowLeftBold, Plus } from "@element-plus/icons-vue";
|
|
|
+import { base_img_url2, resize_300 } from "@/api/config";
|
|
|
+import ImagePreviewPopup from "@/components/popup/ImagePreviewPopup.vue";
|
|
|
+import albumUploadPopup from "../components/albumUploadPopup.vue";
|
|
|
+
|
|
|
+const route = useRoute();
|
|
|
+const router = useRouter();
|
|
|
+
|
|
|
+/** 临时默认参数,后续改成正式接口后可只走 query */
|
|
|
+const TEMP_ORGAN_ID = "101532";
|
|
|
+const TEMP_AREA_ID = "90178";
|
|
|
+const TEMP_DATE = "2026-08-19";
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const imageList = ref([]);
|
|
|
+const showPreview = ref(false);
|
|
|
+const previewImages = ref([]);
|
|
|
+const previewIndex = ref(0);
|
|
|
+const showUploadPopup = ref(false);
|
|
|
+
|
|
|
+const regionName = computed(
|
|
|
+ () => route.query.regionName || route.query.name || "分区一"
|
|
|
+);
|
|
|
+
|
|
|
+const organId = computed(() => route.query.organId || TEMP_ORGAN_ID);
|
|
|
+const areaId = computed(() => route.query.areaId || TEMP_AREA_ID);
|
|
|
+const queryDate = computed(() => route.query.date || TEMP_DATE);
|
|
|
+
|
|
|
+const totalCount = computed(() => imageList.value.length);
|
|
|
+
|
|
|
+const heroCover = computed(() => {
|
|
|
+ const first = imageList.value[0];
|
|
|
+ if (first?.baseMap) return first.baseMap;
|
|
|
+ if (first?.url) return first.url;
|
|
|
+ return "";
|
|
|
+});
|
|
|
+
|
|
|
+const heroStyle = computed(() => {
|
|
|
+ if (!heroCover.value) {
|
|
|
+ return {
|
|
|
+ background:
|
|
|
+ "linear-gradient(180deg, rgba(34, 120, 68, 0.95) 0%, rgba(20, 80, 48, 0.9) 100%)",
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ backgroundImage: `url(${heroCover.value})`,
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const formatDateLabel = (dateStr) => {
|
|
|
+ const raw = String(dateStr || "").slice(0, 10);
|
|
|
+ const parts = raw.split("-");
|
|
|
+ if (parts.length < 3) return raw;
|
|
|
+ return `${parts[0]}年${parts[1]}月${parts[2]}日`;
|
|
|
+};
|
|
|
+
|
|
|
+const getImageUrl = (item) => {
|
|
|
+ if (!item?.filename) return "";
|
|
|
+ if (/^https?:\/\//i.test(item.filename)) return item.filename;
|
|
|
+ return `${base_img_url2}${item.filename}${resize_300 || ""}`;
|
|
|
+};
|
|
|
+
|
|
|
+const getFullImageUrl = (item) => {
|
|
|
+ if (!item?.filename) return "";
|
|
|
+ if (/^https?:\/\//i.test(item.filename)) return item.filename;
|
|
|
+ return `${base_img_url2}${item.filename}`;
|
|
|
+};
|
|
|
+
|
|
|
+const dateGroups = computed(() => {
|
|
|
+ const map = new Map();
|
|
|
+ imageList.value.forEach((item) => {
|
|
|
+ const date = String(item.uploadDate || "").slice(0, 10) || "未知日期";
|
|
|
+ if (!map.has(date)) {
|
|
|
+ map.set(date, {
|
|
|
+ date,
|
|
|
+ dateLabel: formatDateLabel(date),
|
|
|
+ remark: item.growText || item.watermarkMsg || "",
|
|
|
+ list: [],
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const group = map.get(date);
|
|
|
+ if (!group.remark && (item.growText || item.watermarkMsg)) {
|
|
|
+ group.remark = item.growText || item.watermarkMsg;
|
|
|
+ }
|
|
|
+ group.list.push(item);
|
|
|
+ });
|
|
|
+ return [...map.values()].sort((a, b) => String(b.date).localeCompare(String(a.date)));
|
|
|
+});
|
|
|
+
|
|
|
+const normalizeList = (raw) => {
|
|
|
+ const list = Array.isArray(raw)
|
|
|
+ ? raw
|
|
|
+ : Array.isArray(raw?.list)
|
|
|
+ ? raw.list
|
|
|
+ : Array.isArray(raw?.records)
|
|
|
+ ? raw.records
|
|
|
+ : [];
|
|
|
+ return list
|
|
|
+ .map((item) => ({
|
|
|
+ ...item,
|
|
|
+ url: getImageUrl(item),
|
|
|
+ fullUrl: getFullImageUrl(item),
|
|
|
+ }))
|
|
|
+ .filter((item) => item.url);
|
|
|
+};
|
|
|
+
|
|
|
+const fetchAlbumImages = async () => {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ // TODO: 临时接口,后续替换正式相册接口
|
|
|
+ const res = await VE_API.monitor.getRegionAlbumImages({
|
|
|
+ organId: organId.value,
|
|
|
+ areaId: areaId.value,
|
|
|
+ date: queryDate.value,
|
|
|
+ limit: 100,
|
|
|
+ page: 1,
|
|
|
+ });
|
|
|
+ const ok = res?.code === 200 || res?.code === 0 || res?.code === 1 || res?.success;
|
|
|
+ if (ok) {
|
|
|
+ imageList.value = normalizeList(res.data);
|
|
|
+ } else {
|
|
|
+ imageList.value = [];
|
|
|
+ if (res?.msg) ElMessage.error(res.msg);
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ imageList.value = [];
|
|
|
+ ElMessage.error("获取相册失败,请稍后再试");
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const formatPreviewDate = (dateStr) => {
|
|
|
+ const raw = String(dateStr || "").slice(0, 10);
|
|
|
+ return raw ? raw.replace(/-/g, "/") : "";
|
|
|
+};
|
|
|
+
|
|
|
+const previewImage = (group, index) => {
|
|
|
+ previewImages.value = group.list.map((item) => ({
|
|
|
+ url: item.fullUrl || item.url,
|
|
|
+ date: formatPreviewDate(item.uploadDate),
|
|
|
+ // 临时物候标签,有正式字段后替换
|
|
|
+ tag: item.phenologyName || item.tag || "预测物候期",
|
|
|
+ remark: item.growText || item.watermarkMsg || "",
|
|
|
+ id: item.id,
|
|
|
+ }));
|
|
|
+ previewIndex.value = index;
|
|
|
+ showPreview.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleRemarkChange = ({ index, remark }) => {
|
|
|
+ const previewItem = previewImages.value[index];
|
|
|
+ if (!previewItem) return;
|
|
|
+ previewItem.remark = remark;
|
|
|
+};
|
|
|
+
|
|
|
+const handleRemarkConfirm = ({ index, remark, image }) => {
|
|
|
+ const previewItem = previewImages.value[index];
|
|
|
+ if (previewItem) previewItem.remark = remark;
|
|
|
+ // 回写到列表数据(临时本地保存,后续可接保存备注接口)
|
|
|
+ const targetId = image?.id || previewItem?.id;
|
|
|
+ if (targetId == null) return;
|
|
|
+ const target = imageList.value.find((item) => String(item.id) === String(targetId));
|
|
|
+ if (target) {
|
|
|
+ target.growText = remark;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handleAdd = () => {
|
|
|
+ showUploadPopup.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+const handleUploadConfirm = ({ images, description }) => {
|
|
|
+ // TODO: 正式上传相册接口确定后在此提交
|
|
|
+ // images: OSS 相对路径数组;description: 描述文案
|
|
|
+ ElMessage.success(`已选择 ${images.length} 张照片${description ? ",描述已填写" : ""}`);
|
|
|
+ // 临时本地插入,便于联调样式;正式接口返回后改为 fetchAlbumImages()
|
|
|
+ const today = new Date();
|
|
|
+ const y = today.getFullYear();
|
|
|
+ const m = String(today.getMonth() + 1).padStart(2, "0");
|
|
|
+ const d = String(today.getDate()).padStart(2, "0");
|
|
|
+ const uploadDate = `${y}-${m}-${d}`;
|
|
|
+ const localItems = images.map((filename, index) => ({
|
|
|
+ id: `local-${Date.now()}-${index}`,
|
|
|
+ filename,
|
|
|
+ uploadDate,
|
|
|
+ growText: description || "",
|
|
|
+ watermarkMsg: "",
|
|
|
+ url: `${base_img_url2}${filename}${resize_300 || ""}`,
|
|
|
+ fullUrl: `${base_img_url2}${filename}`,
|
|
|
+ }));
|
|
|
+ imageList.value = [...localItems, ...imageList.value];
|
|
|
+};
|
|
|
+
|
|
|
+const goBack = () => {
|
|
|
+ router.back();
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchAlbumImages();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.region-albums {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f5f7fb;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &__hero {
|
|
|
+ position: relative;
|
|
|
+ height: 160px;
|
|
|
+ background-size: cover;
|
|
|
+ background-position: center;
|
|
|
+ background-repeat: no-repeat;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__hero-mask {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ background: linear-gradient(
|
|
|
+ 180deg,
|
|
|
+ rgba(0, 0, 0, 0.25) 0%,
|
|
|
+ rgba(0, 0, 0, 0.05) 45%,
|
|
|
+ rgba(0, 0, 0, 0.35) 100%
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ &__nav {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 2;
|
|
|
+ top: 14px;
|
|
|
+ left: 12px;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(0, 0, 0, 0.25);
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__hero-info {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 2;
|
|
|
+ left: 10px;
|
|
|
+ bottom: 36px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__title {
|
|
|
+ font-size: 18px;
|
|
|
+ line-height: 27px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #fff;
|
|
|
+ text-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
|
|
+ }
|
|
|
+
|
|
|
+ &__count {
|
|
|
+ padding: 0 6px;
|
|
|
+ height: 24px;
|
|
|
+ line-height: 24px;
|
|
|
+ border-radius: 4px;
|
|
|
+ backdrop-filter: blur(4px);
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ color: #fff;
|
|
|
+ font-size: 14px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__panel {
|
|
|
+ position: relative;
|
|
|
+ z-index: 3;
|
|
|
+ margin-top: -20px;
|
|
|
+ height: calc(100vh - 140px);
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 12px 12px 0 0;
|
|
|
+ padding-top: 12px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__empty {
|
|
|
+ padding: 60px 0;
|
|
|
+ text-align: center;
|
|
|
+ color: rgba(0, 0, 0, 0.35);
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__album-list {
|
|
|
+ overflow-y: auto;
|
|
|
+ height: 100%;
|
|
|
+ padding: 0 6px 40px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__fab {
|
|
|
+ position: fixed;
|
|
|
+ right: 18px;
|
|
|
+ bottom: 28px;
|
|
|
+ z-index: 20;
|
|
|
+ width: 56px;
|
|
|
+ height: 56px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #2199f8;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ box-shadow: 0 6px 16px rgba(33, 153, 248, 0.4);
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.album-group {
|
|
|
+ & + & {
|
|
|
+ margin-top: 22px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__header {
|
|
|
+ margin-bottom: 14px;
|
|
|
+ padding: 0 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__date {
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 22px;
|
|
|
+ color: #010101;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__remark {
|
|
|
+ margin-top: 2px;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 18px;
|
|
|
+ color: rgba(1, 1, 1, 0.5);
|
|
|
+ }
|
|
|
+
|
|
|
+ &__grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__item {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ padding-top: 100%;
|
|
|
+ border-radius: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f2f2f2;
|
|
|
+ }
|
|
|
+
|
|
|
+ &__img {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|