riku
2025-09-17 4aa86b1ec441c4e358e1cc488d8f021fb80f1355
2025.9.17 数据产品(待完成)
已修改7个文件
已添加2个文件
495 ■■■■ 文件已修改
src/api/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/css-util.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/ProdManage.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/ProdSceneInfo.vue 83 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/components/BaseProdProcess.vue 318 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/components/ProdDownload.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/components/ProdQueryOpt.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js
@@ -13,8 +13,8 @@
let ip2_file = 'https://fyami.com.cn/';
if (debug) {
  // ip1 = 'http://192.168.0.103:9001/';
  ip1 = 'http://localhost:9001/';
  ip1 = 'http://192.168.0.103:9001/';
  // ip1 = 'http://localhost:9001/';
  // ip1_file = 'http://192.168.0.138:8080/';
  // ip2 = 'http://192.168.0.138:8080/';
  // ip2_file = 'https://fyami.com.cn/';
src/components.d.ts
@@ -20,11 +20,8 @@
    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElCalendar: typeof import('element-plus/es')['ElCalendar']
    ElCard: typeof import('element-plus/es')['ElCard']
    ElCascader: typeof import('element-plus/es')['ElCascader']
    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElCollapse: typeof import('element-plus/es')['ElCollapse']
    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
@@ -45,7 +42,6 @@
    ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
    ElLink: typeof import('element-plus/es')['ElLink']
    ElMain: typeof import('element-plus/es')['ElMain']
    ElMenu: typeof import('element-plus/es')['ElMenu']
    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
src/router/index.js
@@ -93,7 +93,7 @@
                        // åŸºç¡€äº§å“-场景清单
                        path: 'scene',
                        name: 'ProdSceneInfo',
                        meta: { keepAlive: true, key: 'ProdManage' },
                        meta: { keepAlive: false, key: 'ProdManage' },
                        component: () =>
                          import(
                            '@/views/fysp/data-product/base-data-product/ProdSceneInfo.vue'
@@ -103,7 +103,7 @@
                        // åŸºç¡€äº§å“-规范性评估
                        path: 'evaluate',
                        name: 'ProdEvaluationInfo',
                        meta: { keepAlive: true, key: 'ProdManage' },
                        meta: { keepAlive: false, key: 'ProdManage' },
                        component: () =>
                          import(
                            '@/views/fysp/data-product/base-data-product/ProdEvaluationInfo.vue'
@@ -113,7 +113,7 @@
                        // åŸºç¡€äº§å“-巡查信息
                        path: 'inspection',
                        name: 'ProdInspectionInfo',
                        meta: { keepAlive: true, key: 'ProdManage' },
                        meta: { keepAlive: false, key: 'ProdManage' },
                        component: () =>
                          import(
                            '@/views/fysp/data-product/base-data-product/ProdInspectionInfo.vue'
@@ -123,7 +123,7 @@
                        // åŸºç¡€äº§å“-监测数据
                        path: 'monitordata',
                        name: 'ProdMonitorDataInfo',
                        meta: { keepAlive: true, key: 'ProdManage' },
                        meta: { keepAlive: false, key: 'ProdManage' },
                        component: () =>
                          import(
                            '@/views/fysp/data-product/base-data-product/ProdMonitorDataInfo.vue'
src/utils/css-util.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
/**
 * æå–css中带有calc函数的表达式的参数部分
 * @param {String} str css中带有calc函数的表达式
 */
function unCalc(str) {
  if (str.startsWith('calc(')) {
    let _str = str.replace('calc(', '')
    _str = _str.replace(/\)/g, (match, offset, string) => {
      if (offset === string.lastIndexOf(match)) {
        return ''
      } else {
        return match
      }
    })
    return _str
  } else {
    return str
  }
}
export { unCalc }
src/views/fysp/data-product/base-data-product/ProdManage.vue
@@ -1,19 +1,22 @@
<template>
  <el-menu
    default-active="scene"
    ellipsis
    mode="horizontal"
    style="max-width: 600px"
  >
    <el-menu-item
      v-for="item in menu"
      :key="item.path"
      :index="item.path"
      @click="navPage"
      >{{ item.name }}</el-menu-item
  <el-affix>
    <el-menu
      ref="menuRef"
      default-active="scene"
      ellipsis
      mode="horizontal"
      style="max-width: 600px; background-color: aliceblue"
    >
  </el-menu>
  <router-view v-slot="{ Component, route }">
      <el-menu-item
        v-for="item in menu"
        :key="item.path"
        :index="item.path"
        @click="navPage"
        >{{ item.name }}</el-menu-item
      >
    </el-menu>
  </el-affix>
  <router-view v-slot="{ Component, route }" :style="'height: ' + height">
    <keep-alive>
      <component
        v-if="route.meta.keepAlive"
@@ -25,11 +28,14 @@
  </router-view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, provide } from 'vue';
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
const menuRef = ref(null);
const height = ref('calc(100vh - 64px)');
const menu = ref([
  {
@@ -58,5 +64,17 @@
    });
  }
};
function calcTableHeight() {
  const h = menuRef.value.$el.offsetHeight;
  return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
}
onMounted(() => {
  height.value = calcTableHeight();
});
// æä¾›ç»™å†…部组件视图最大高度
provide('viewHeight', height);
</script>
<style scoped></style>
src/views/fysp/data-product/base-data-product/ProdSceneInfo.vue
@@ -1,31 +1,90 @@
<template>
  <BaseProdProcess :active="active">
    <template #step1>
  <BaseProdProcess
    v-model:active="active"
    @onStep1="onStep1"
    :loading="loading"
  >
    <!-- <template #step1>
      <ProdQueryOpt :loading="loading" @submit="onSearch"> </ProdQueryOpt>
    </template> -->
    <template #step2="{ contentHeight }">
      <el-table
        :data="tableData"
        v-loading="loading"
        :height="viewHeight"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        size="small"
        border
      >
        <el-table-column fixed="left" prop="index" label="编号" width="40">
        </el-table-column>
        <el-table-column
          fixed="left"
          prop="name"
          label="名称"
          :show-overflow-tooltip="true"
          min-width="200"
        >
        </el-table-column>
        <el-table-column prop="type" label="类型" width="50" />
        <el-table-column prop="status" label="状态" width="60" />
        <el-table-column prop="stage" label="阶段" width="90" />
        <!-- <el-table-column prop="provincename" label="省" width="90" />
        <el-table-column prop="cityname" label="市" width="90" />
        <el-table-column prop="districtname" label="区县" width="90" /> -->
        <el-table-column prop="townname" label="街道" width="110" />
        <el-table-column prop="location" label="地址" width="200" />
        <!-- <el-table-column prop="longitude" label="经度" width="110" />
        <el-table-column prop="latitude" label="纬度" width="110" /> -->
        <!-- <el-table-column
          prop="updatedate"
          label="更新时间"
          width="140"
          :formatter="timeFormat"
        /> -->
      </el-table>
    </template>
    <template #step2></template>
    <template #step3></template>
    <!-- <template #step3></template> -->
  </BaseProdProcess>
</template>
<script setup>
import { ref } from 'vue';
import { ref, inject } from 'vue';
import dayjs from 'dayjs';
import BaseProdProcess from '@/views/fysp/data-product/base-data-product/components/BaseProdProcess.vue';
import ProdQueryOpt from '@/views/fysp/data-product/base-data-product/components/ProdQueryOpt.vue';
import dataprodbaseApi from '@/api/fysp/dataprodbaseApi.js';
const active = ref(1);
const loading = ref(false)
const loading = ref(false);
const tableData = ref([]);
const viewHeight = inject('viewHeight');
function changeActive() {
  active.value++;
  active.value = active.value > 3 ? 1 : active.value;
}
function onSearch(opt) {
  console.log(opt);
function onStep1(opt) {
  loading.value = true;
  setTimeout(() => {
    changeActive()
    loading.value = false;
  }, 1000);
  dataprodbaseApi
    .fetchProdSceneInfo(opt)
    .then((res) => {
      if (res.success) {
        tableData.value = res.data.map((item) => ({
          status: item.status,
          stage: item.stage,
          ...item.scene
        }));
      }
      changeActive();
    })
    .finally(() => {
      loading.value = false;
    });
}
function timeFormat(row, column, cellValue, index) {
  return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
}
</script>
src/views/fysp/data-product/base-data-product/components/BaseProdProcess.vue
@@ -1,62 +1,310 @@
<template>
  <!-- <el-button type="primary" @click="changeActive">change</el-button> -->
  <!-- <el-row>
    <el-col
      :span="active == 1 ? 16 : 4"
      :class="active == 1 ? 'prod-active' : 'prod-inactive'"
    >step1</el-col>
    <el-col
      :span="active == 2 ? 16 : 4"
      :class="active == 2 ? 'prod-active' : 'prod-inactive'"
    >step2</el-col>
    <el-col
      :span="active == 3 ? 16 : 4"
      :class="active == 3 ? 'prod-active' : 'prod-inactive'"
    >step3</el-col>
  </el-row> -->
  <el-row>
    <div :class="active == 1 ? 'prod-active' : 'prod-inactive'">
        <slot name="step1"></slot>
    <!-- æ­¥éª¤1 æ•°æ®äº§å“ç”Ÿæˆé€‰é¡¹ -->
    <div :class="active == 1 ? 'prod-active' : 'prod-inactive'" ref="step1Ref">
      <transition
        name="el-fade-in"
        @after-leave="handleTransitionContentEnd(1)"
      >
        <div v-show="showStep1Content">
          <template v-if="$slots.step1">
            <slot name="step1"></slot>
          </template>
          <template v-else>
            <ProdQueryOpt :loading="loading" @submit="onSearch"> </ProdQueryOpt>
          </template>
        </div>
      </transition>
      <transition
        name="el-fade-in"
        @after-leave="handleTransitionThumbnailEnd(1)"
      >
        <div
          v-show="showStep1Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight }"
          @click="changeActive(1)"
        >
          <div class="prod-thumbnail">①修改选项</div>
        </div>
      </transition>
    </div>
    <div :class="active == 2 ? 'prod-active' : 'prod-inactive'">
        <slot name="step2"></slot>
    <!-- æ­¥éª¤2 æ•°æ®äº§å“ç»“果预览 -->
    <div :class="active == 2 ? 'prod-active' : 'prod-inactive'" ref="step2Ref">
      <transition
        name="el-fade-in"
        @after-leave="handleTransitionContentEnd(2)"
      >
        <div v-show="showStep2Content">
          <div ref="titleRef" class="prod-title">
            <el-text tag="b" size="large">数据产品预览</el-text>
          </div>
          <slot name="step2" :contentHeight="contentHeight"></slot>
        </div>
      </transition>
      <transition
        name="el-fade-in"
        @after-leave="handleTransitionThumbnailEnd(2)"
      >
        <div
          v-show="showStep2Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight }"
          @click="changeActive(2)"
        >
          <div
            :class="
              'prod-thumbnail prod-thumbnail_middle ' +
              (active < 2 ? 'prod-thumbnail-disabled' : '')
            "
          >
            â‘¡æ•°æ®äº§å“é¢„览
          </div>
        </div>
      </transition>
    </div>
    <div :class="active == 3 ? 'prod-active' : 'prod-inactive'">
        <slot name="step3"></slot>
    <!-- æ­¥éª¤3 æ•°æ®äº§å“è¡¨å•下载 -->
    <div :class="active == 3 ? 'prod-active' : 'prod-inactive'" ref="step3Ref">
      <transition
        name="el-fade-in"
        @after-leave="handleTransitionContentEnd(3)"
      >
        <div v-show="showStep3Content">
          <template v-if="$slots.step3">
            <slot name="step3"></slot>
          </template>
          <template v-else>
            <ProdDownload></ProdDownload>
          </template>
        </div>
      </transition>
      <transition
        name="el-fade-in"
        @after-leave="handleTransitionThumbnailEnd(3)"
      >
        <div
          v-show="showStep3Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight }"
          @click="changeActive(3)"
        >
          <div
            :class="
              'prod-thumbnail prod-thumbnail_end ' +
              (active < 3 ? 'prod-thumbnail-disabled' : '')
            "
          >
            â‘¢æ•°æ®äº§å“ä¸‹è½½
          </div>
        </div>
      </transition>
    </div>
  </el-row>
</template>
<script setup>
import { ref } from 'vue';
import { computed, inject, ref, watch, onMounted } from 'vue';
import { unCalc } from '@/utils/css-util';
import ProdQueryOpt from '@/views/fysp/data-product/base-data-product/components/ProdQueryOpt.vue';
import ProdDownload from '@/views/fysp/data-product/base-data-product/components/ProdDownload.vue';
const props = defineProps({
  active: {
    type: Number,
    default: 1
  },
  loading: {
    type: Boolean,
    default: false
  }
});
// function changeActive() {
//   active.value++;
//   active.value = active.value > 3 ? 1 : active.value;
// }
const emit = defineEmits(['update:active', 'onStep1']);
const viewHeight = inject('viewHeight');
const btnDisabled = ref(false);
const titleRef = ref(null);
const contentHeight = ref('50vh');
function calContentHeight() {
  console.log(titleRef.value.offsetHeight);
  contentHeight.value = `calc(${unCalc(viewHeight.value)} - ${
    titleRef.value?.offsetHeight || 0
  }px)`;
  console.log(contentHeight.value);
}
// æ­¥éª¤å¼•用
const step1Ref = ref(null);
const step2Ref = ref(null);
const step3Ref = ref(null);
// æŽ§åˆ¶æ˜¾ç¤º/隐藏的状态
const showStep1Content = ref(props.active === 1);
const showStep1Thumbnail = ref(props.active != 1);
const showStep2Content = ref(props.active === 2);
const showStep2Thumbnail = ref(props.active != 2);
const showStep3Content = ref(props.active === 3);
const showStep3Thumbnail = ref(props.active != 3);
// è®°å½•动画是否正在进行中
const isAnimating = ref({});
// ç›‘听active变化
watch(
  () => props.active,
  (newActive, oldActive) => {
    // æ ‡è®°åŠ¨ç”»å¼€å§‹
    isAnimating.value[oldActive] = true;
    isAnimating.value[newActive] = true;
    // å…ˆéšè—æ‰€æœ‰å†…容,等待动画结束后再显示正确的内容
    if (oldActive === 1) {
      showStep1Content.value = false;
    } else if (oldActive === 2) {
      showStep2Content.value = false;
    } else if (oldActive === 3) {
      showStep3Content.value = false;
    }
    if (newActive === 1) {
      showStep1Thumbnail.value = false;
    } else if (newActive === 2) {
      showStep2Thumbnail.value = false;
    } else if (newActive === 3) {
      showStep3Thumbnail.value = false;
    }
  }
);
// å¤„理动画结束事件
function handleTransitionThumbnailEnd(stepIndex) {
  // æ£€æŸ¥åŠ¨ç”»æ˜¯å¦ç¡®å®žç»“æŸï¼ˆé¿å…é‡å¤è§¦å‘ï¼‰
  if (isAnimating.value[stepIndex]) {
    isAnimating.value[stepIndex] = false;
    // setTimeout(() => {
    // åŠ¨ç”»ç»“æŸåŽï¼Œæ›´æ–°æ˜¾ç¤ºçŠ¶æ€
    if (stepIndex === 1) {
      showStep1Content.value = props.active === 1;
    } else if (stepIndex === 2) {
      showStep2Content.value = props.active === 2;
    } else if (stepIndex === 3) {
      showStep3Content.value = props.active === 3;
    }
    // }, 50);
  }
}
function handleTransitionContentEnd(stepIndex) {
  // æ£€æŸ¥åŠ¨ç”»æ˜¯å¦ç¡®å®žç»“æŸï¼ˆé¿å…é‡å¤è§¦å‘ï¼‰
  if (isAnimating.value[stepIndex]) {
    isAnimating.value[stepIndex] = false;
    // setTimeout(() => {
    // åŠ¨ç”»ç»“æŸåŽï¼Œæ›´æ–°æ˜¾ç¤ºçŠ¶æ€
    if (stepIndex === 1) {
      showStep1Thumbnail.value = props.active != 1;
    } else if (stepIndex === 2) {
      showStep2Thumbnail.value = props.active != 2;
    } else if (stepIndex === 3) {
      showStep3Thumbnail.value = props.active != 3;
    }
    // }, 50);
  }
}
function onSearch(opt) {
  emit('onStep1', opt);
}
function changeActive(index) {
  let isAnimate = false;
  Object.values(isAnimating.value).forEach((item) => {
    isAnimate = isAnimate || item;
  });
  if (!isAnimate && !btnDisabled.value && props.active >= index) {
    emit('update:active', index);
    btnDisabled.value = true;
    setTimeout(() => {
      btnDisabled.value = false;
    }, 500);
  }
  // emit('update:active', index);
}
onMounted(() => {
  calContentHeight();
});
</script>
<style scoped>
.prod-active {
  width: 66.667%;
  /* width: 66.667%; */
  width: 90%;
  transition:
    width 0.5s ease,
    box-shadow 0.3s ease;
  /* background-color: #409eff; */
  margin: 5px 0;
  border-radius: 4px;
  box-shadow:
    -3px 0 6px rgba(0, 0, 0, 0.1),
    3px 0 6px rgba(0, 0, 0, 0.1);
}
.prod-inactive {
  /* width: 16.667%; */
  width: 5%;
  transition: width 0.5s ease;
  background-color: #409eff;
  color: white;
  /* background-color: #e4e7ed; */
  margin: 5px 0;
  border-radius: 4px;
}
.prod-inactive {
  width: 16.667%;
  transition: width 0.5s ease;
.prod-title {
  padding: 10px;
}
.prod-thumbnail-wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0 2px;
}
.prod-thumbnail {
  height: 90%;
  width: 100%;
  background-color: #409eff;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  writing-mode: vertical-rl;
  text-orientation: upright;
  letter-spacing: 8px;
  font-size: 18px;
  font-weight: 500;
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  cursor: pointer;
}
.prod-thumbnail_middle {
  border-radius: 0px;
}
.prod-thumbnail_end {
  border-top-left-radius: 0px;
  border-bottom-left-radius: 0px;
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
.prod-thumbnail-disabled {
  background-color: #e4e7ed;
  color: #606266;
  margin: 5px 0;
  border-radius: 4px;
  color: #c0c4cc;
  cursor: not-allowed;
}
</style>
src/views/fysp/data-product/base-data-product/components/ProdDownload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
<template>ProdDownload</template>
<script setup></script>
src/views/fysp/data-product/base-data-product/components/ProdQueryOpt.vue
@@ -4,6 +4,7 @@
      <div><el-text tag="b" size="large">产品生成选项</el-text></div>
    </template>
    <SearchBar
      v-show="active"
      ref="refSearchBar"
      :btn-show="false"
      :init="false"
@@ -11,7 +12,7 @@
    >
    </SearchBar>
    <template #footer>
      <el-row justify="space-around">
      <el-row v-show="active" justify="space-around">
        <el-button
          type="primary"
          size="default"
@@ -31,6 +32,10 @@
  loading: {
    type: Boolean,
    default: false
  },
  active:{
    type: Boolean,
    default: true
  }
});
const emit = defineEmits(['submit']);