riku
2025-09-18 c1d2051abc8ca88cd07f0d7c56c0dbf8165d5c33
2025.9.18 数据产品(待完成)
已修改21个文件
已添加2个文件
已重命名1个文件
855 ■■■■ 文件已修改
src/api/fysp/nightConstructionApi.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SearchBar.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SideList.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ToolBar.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/core/BaseContentLayout.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/search-option/FYSearchBar.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/table/FYTable.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/HomePage.vue 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/check/ProCheck.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/config/components/CompInfoSearch.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/config/components/CompInfoSearchFysp.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/config/device/CompDeviceMatchEdit.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/ProdEvaluationInfo.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/ProdInspectionInfo.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/ProdManage.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/ProdSceneInfo.vue 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/components/BaseProdProcess.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/components/ProdDownload.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/base-data-product/components/ProdQueryOpt.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/data-product/prod-step-change.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/support/JingAnNightConstruction.vue 135 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/support/JingAnSupport.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/fysp/nightConstructionApi.js
@@ -13,5 +13,18 @@
        params: { cityCode, districtCode, page, perPage }
      })
      .then((res) => res.data);
  },
  /**
   * æ›´æ–°å·¥åœ°å¤œé—´æ–½å·¥è®¸å¯è¯
   * @param {*} param0
   * @returns
   */
  updateRecord({ recordId, userId, sceneId }) {
    return $fysp
      .post(`nightwork/record`, undefined, {
        params: { recordId, userId, sceneId }
      })
      .then((res) => res.data);
  }
};
src/components.d.ts
@@ -20,8 +20,11 @@
    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']
@@ -42,11 +45,13 @@
    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']
    ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
    ElPagination: typeof import('element-plus/es')['ElPagination']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElRadio: typeof import('element-plus/es')['ElRadio']
@@ -54,6 +59,7 @@
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSegmented: typeof import('element-plus/es')['ElSegmented']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElSpace: typeof import('element-plus/es')['ElSpace']
    ElStep: typeof import('element-plus/es')['ElStep']
@@ -67,6 +73,7 @@
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    ElTooltip: typeof import('element-plus/es')['ElTooltip']
    ElTransfer: typeof import('element-plus/es')['ElTransfer']
    ElTree: typeof import('element-plus/es')['ElTree']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    Footer: typeof import('./components/core/Footer.vue')['default']
src/components/SearchBar.vue
@@ -1,6 +1,6 @@
<template>
  <el-row class="layout">
    <el-col :span="10">
    <el-col :span="$slots.summary ? 10 : 24">
      <el-form :inline="true" :model="formSearch">
        <el-form-item label="总任务">
          <!-- <el-input v-model="formSearch.topTaskId" placeholder="总任务" /> -->
@@ -27,7 +27,7 @@
        </el-form-item>
      </el-form>
    </el-col>
    <el-col :span="14">
    <el-col :span="$slots.summary ? 14 : 0">
      <el-row justify="end">
        <slot name="summary"></slot>
      </el-row>
@@ -85,7 +85,8 @@
      );
      const param = {
        topTask: task ? task.data : {},
        sceneTypeId: this.formSearch.scenetype.value
        sceneTypeId: this.formSearch.scenetype.value,
        sceneTypeName: this.formSearch.scenetype.label,
      };
      // console.log(param);
src/components/SideList.vue
@@ -1,16 +1,21 @@
<template>
  <div style="padding-right: 10px;">
  <div v-if="legend" class="state-label">
    <el-input
      v-model="filterText"
      icon="Search"
      style="width: 200px"
        style="width: 180px"
      placeholder="关键字筛选"
      clearable
    />
    <el-tooltip placement="bottom-start" effect="dark">
      <template #content>
        <el-space>
          <el-space v-for="(item, index) in stateLabels" :key="index" :size="1">
            <el-space
              v-for="(item, index) in stateLabels"
              :key="index"
              :size="1"
            >
            <el-icon :color="item.color">
              <component :is="item.icon"></component>
            </el-icon>
@@ -142,6 +147,7 @@
      </slot>
    </template>
  </el-tree>
  </div>
</template>
<script>
src/components/ToolBar.vue
@@ -60,7 +60,7 @@
</script>
<style scoped>
.layout {
  /* background-color: beige; */
  background-color: white;
  height: var(--height-toolbar);
  border-bottom: 1px solid var(--el-color-info-light-7);
  box-shadow: 6px 4px 4px rgba(0, 0, 0, 0.12);
src/components/core/BaseContentLayout.vue
@@ -4,12 +4,12 @@
      <slot name="header"></slot>
    </el-header>
    <el-container>
      <el-aside class="el-aside" :style="'height: ' + mainHeight">
        <el-scrollbar :noresize="true" style="position: relative;">
      <el-aside class="el-aside" :style="{ height: mainHeight + 'px' }">
        <el-scrollbar :noresize="false">
          <slot name="aside"></slot>
        </el-scrollbar>
      </el-aside>
      <el-main class="el-main" :style="'height: ' + mainHeight">
      <el-main class="el-main" :style="{ height: mainHeight + 'px' }">
        <slot name="main"></slot>
      </el-main>
    </el-container>
@@ -18,10 +18,11 @@
<script>
export default {
  inject: ['contentMaxHeight'],
  // å³ä¾§æ“ä½œç•Œé¢åŸºç¡€å¸ƒå±€
  data() {
    return {
      mainHeight: 'calc(100vh - 60px * 2 - var(--el-main-padding) * 2)'
      mainHeight: this.contentMaxHeight.value
    };
  },
  methods: {
@@ -30,9 +31,10 @@
      if (this.$refs.headerRef) {
        const h1 = this.$refs.headerRef.$el.offsetHeight;
        const h = h1;
        return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2 + 6px)`;
        return this.contentMaxHeight.value - h;
        // return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2 + 6px)`;
      } else {
        return `calc(100vh - 60px * 2 - var(--el-main-padding) * 2)`;
        return this.contentMaxHeight.value;
      }
    }
  },
@@ -58,10 +60,10 @@
.el-header {
  height: initial;
  padding: 0 0 0px 0;
  padding: 0 0 4px 0;
  /* background-color: rgb(216, 201, 201); */
  /* border-bottom: 1px solid var(--el-color-info-light-7); */
  margin-bottom: 4px;
  /* margin-bottom: 4px; */
}
.el-main {
@@ -69,7 +71,7 @@
  /* background-color: whitesmoke; */
  /* height: calc(100vh - 60px * 2 - 20px * 2); */
  padding: initial;
  padding-left: 20px;
  padding-left: 10px;
  /* overflow: hidden; */
}
</style>
src/components/search-option/FYSearchBar.vue
@@ -1,7 +1,7 @@
<template>
  <el-form :inline="true" :size="size">
    <slot name="options"></slot>
    <el-form-item>
    <el-form-item v-if="$slots.options">
      <el-button
        icon="Search"
        type="primary"
src/components/table/FYTable.vue
@@ -1,10 +1,10 @@
<template>
  <el-row ref="searchRef">
    <FYSearchBar @search="onSearch">
      <template #options>
      <template #options v-if="$slots.options">
        <slot name="options"></slot>
      </template>
      <template #buttons>
      <template #buttons v-if="$slots.buttons">
        <slot name="buttons"></slot>
      </template>
    </FYSearchBar>
@@ -69,6 +69,7 @@
 * ä½¿ç”¨æ—¶éœ€è¦åœ¨<slot #options>中添加自定义查询选项,在<slot #table-column>中添加自定义表格列,同时实现触发函数search
 */
export default {
  inject: ['contentMaxHeight'],
  props: {
    rowClassName: undefined,
    cellClassName: Function || String,
@@ -86,6 +87,11 @@
      default: () => []
    },
    totalCount: {
      type: Number,
      default: 0
    },
    // é¢å¤–的高度,用于计算表格高度
    extraHeight: {
      type: Number,
      default: 0
    }
@@ -130,26 +136,16 @@
      if (nValue != oValue) {
        this.total = nValue;
      }
    },
    extraHeight: {
      handler(nValue, oValue) {
        if (nValue != oValue) {
          this.tableHeight = this.calcTableHeight();
    }
  },
  computed: {
    cTableHeight() {
      if (this.$refs.searchRef) {
        const h1 = this.$refs.searchRef.$el.offsetHeight;
        const h2 = this.$refs.paginationRef
          ? this.$refs.paginationRef.$el.offsetHeight
          : 0;
        const h3 = this.$refs.expandRef.$el.offsetHeight;
        const h4 = this.$refs.expand2Ref.offsetHeight;
        const h = h1 + h2 + h3 + h4;
        // return `calc(100vh - ${h1}px - ${h2}px - var(--el-main-padding) * 2 - var(--el-header-height))`;
        return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
      } else {
        return '500';
      }
    }
  },
  computed: {},
  methods: {
    /**
     * è¡¨æ ¼æ•°æ®æŸ¥è¯¢ï¼Œä¼ é€’两组参数,分页信息和回调函数
@@ -186,9 +182,9 @@
      const h3 = this.$refs.expandRef.$el.offsetHeight;
      const h4 = this.$refs.expand2Ref.offsetHeight;
      const h = h1 + h2 + h3 + h4;
      // return `calc(100vh - ${h1}px - ${h2}px - var(--el-main-padding) * 2 - var(--el-header-height))`;
      return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
      const h = h1 + h2 + h3 + h4 + this.extraHeight;
      return this.contentMaxHeight.value - h + 'px';
      // return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
    },
    tableRowClassName({ row }) {
      if (this.rowClassName) {
@@ -220,7 +216,7 @@
  mounted() {
    this.tableHeight = this.calcTableHeight();
    this.onSearch();
  },
  }
};
</script>
src/router/index.js
@@ -93,7 +93,7 @@
                        // åŸºç¡€äº§å“-场景清单
                        path: 'scene',
                        name: 'ProdSceneInfo',
                        meta: { keepAlive: false, key: 'ProdManage' },
                        meta: { keepAlive: true, key: 'ProdManage' },
                        component: () =>
                          import(
                            '@/views/fysp/data-product/base-data-product/ProdSceneInfo.vue'
@@ -103,7 +103,7 @@
                        // åŸºç¡€äº§å“-规范性评估
                        path: 'evaluate',
                        name: 'ProdEvaluationInfo',
                        meta: { keepAlive: false, key: 'ProdManage' },
                        meta: { keepAlive: true, key: 'ProdManage' },
                        component: () =>
                          import(
                            '@/views/fysp/data-product/base-data-product/ProdEvaluationInfo.vue'
@@ -113,7 +113,7 @@
                        // åŸºç¡€äº§å“-巡查信息
                        path: 'inspection',
                        name: 'ProdInspectionInfo',
                        meta: { keepAlive: false, key: 'ProdManage' },
                        meta: { keepAlive: true, key: 'ProdManage' },
                        component: () =>
                          import(
                            '@/views/fysp/data-product/base-data-product/ProdInspectionInfo.vue'
@@ -123,7 +123,7 @@
                        // åŸºç¡€äº§å“-监测数据
                        path: 'monitordata',
                        name: 'ProdMonitorDataInfo',
                        meta: { keepAlive: false, key: 'ProdManage' },
                        meta: { keepAlive: true, key: 'ProdManage' },
                        component: () =>
                          import(
                            '@/views/fysp/data-product/base-data-product/ProdMonitorDataInfo.vue'
@@ -323,8 +323,8 @@
];
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  // history: createWebHashHistory(),
  // history: createWebHistory(import.meta.env.BASE_URL),
  history: createWebHashHistory(),
  routes: routes
});
src/views/HomePage.vue
@@ -1,11 +1,11 @@
<template>
  <el-container class="el-container">
    <el-aside class="el-aside"
      ><SiderMenu :collapse="isCollapsed" @nav-page="navPage"></SiderMenu
    ></el-aside>
    <el-aside class="el-aside">
      <SiderMenu :collapse="isCollapsed" @nav-page="navPage"></SiderMenu>
    </el-aside>
    <el-container>
      <el-header class="el-header"
        ><Header
      <el-header ref="headerRef" class="el-header">
        <Header
          :navTitles="navTitles"
          :collapse="isCollapsed"
          @collapsed-sider="collapsedSider"
@@ -13,7 +13,13 @@
      ></el-header>
      <el-main class="el-main">
        <el-scrollbar>
          <div class="el-main__content">
          <div
            class="el-main__content"
            :style="{
              maxHeight: contentMaxHeight + 'px',
              padding: mainPadding + 'px'
            }"
          >
            <Content></Content>
            <!-- <el-backtop
                target=".el-main .el-scrollbar__wrap"
@@ -34,11 +40,16 @@
</template>
<script>
import { computed } from 'vue';
export default {
  data() {
    return {
      isCollapsed: false,
      navTitles: []
      navTitles: [],
      headerHeight: 60,
      mainPadding: 10,
      contentMaxHeight: NaN
    };
  },
  methods: {
@@ -48,6 +59,18 @@
    navPage(titles) {
      this.navTitles = titles;
    }
  },
  mounted() {
    this.headerHeight = this.$refs.headerRef.$el.offsetHeight;
    this.contentMaxHeight =
      window.innerHeight - this.headerHeight - this.mainPadding * 2;
  },
  provide() {
    return {
      headerHeight: computed(() => this.headerHeight),
      mainPadding: computed(() => this.mainPadding),
      contentMaxHeight: computed(() => this.contentMaxHeight)
    };
  }
};
</script>
@@ -74,8 +97,10 @@
}
.el-main__content {
  padding: var(--el-main-padding) calc(var(--el-main-padding) / 2);
  max-height: calc(100vh - 60px - var(--el-main-padding) * 2);
  /* --main-padding: 10px; */
  /* padding: var(--main-padding) calc(var(--main-padding) / 2); */
  /* padding: var(--main-padding); */
  /* max-height: calc(100vh - 60px - var(--main-padding) * 2); */
  /* background-color: aqua; */
  /* overflow: auto; */
}
src/views/fysp/check/ProCheck.vue
@@ -23,16 +23,14 @@
    <template #main>
      <el-scrollbar>
        <ToolBar
          ref="toolBarRef"
          class="toolbar-sticky"
          :title="curSubtask.title"
          :descriptions="proStatus"
          :buttons="buttons"
          :loading="mainLoading"
        ></ToolBar>
        <el-scrollbar
          v-if="curProList.length > 0"
          class="scrollbar-inner"
          v-loading="mainLoading"
        >
        <div v-if="curProList.length > 0" v-loading="mainLoading">
          <CompProblemCard
            :key="i"
            v-for="(p, i) in curProList"
@@ -43,7 +41,7 @@
            @submit="updateSubtask"
            @check="handleProblemCheck"
          ></CompProblemCard>
        </el-scrollbar>
        </div>
        <el-empty v-else description="暂无问题" v-loading="mainLoading" />
      </el-scrollbar>
    </template>
@@ -290,4 +288,10 @@
.scrollbar-inner {
  height: calc(100vh - 60px * 2 - 20px * 2 - var(--height-toolbar));
}
.toolbar-sticky {
  position: sticky;
  z-index: 2;
  top: 0;
}
</style>
src/views/fysp/config/components/CompInfoSearch.vue
ÎļþÃû´Ó src/views/fysp/config/device/CompInfoSearch.vue ÐÞ¸Ä
@@ -15,7 +15,7 @@
        label=""
        :placeholder="placeholder"
        v-model:value="searchText"
        width="200px"
        :width="searchTextWidth"
      ></FYOptionText>
    </template>
  </FYSearchBar>
@@ -27,6 +27,7 @@
        </div>
      </el-space>
    </el-scrollbar>
    <el-empty v-else description="无记录" />
  </div>
  <el-pagination
    v-if="pageShow && dataList.length > 0"
@@ -64,6 +65,10 @@
      type: String,
      default: '输入关键字检索'
    },
    searchTextWidth: {
      type: String,
      default: '200px'
    },
    // æ˜¯å¦æ˜¾ç¤ºåˆ†é¡µ
    pageShow: {
      type: Boolean,
@@ -75,7 +80,9 @@
      default: () => {
        return [10, 20, 50, 100];
      }
    }
    },
    // é»˜è®¤æœç´¢æ–‡æœ¬
    defaultText: String
  },
  emits: ['search', 'update:modelValue'],
  data() {
@@ -86,7 +93,19 @@
      loading: false
    };
  },
  watch: {},
  watch: {
    defaultText: {
      handler(newVal) {
        if (newVal) {
          this.searchText = newVal;
          setTimeout(() => {
            this.search();
          }, 500);
        }
      },
      immediate: true
    }
  },
  methods: {
    search() {
      this.loading = true;
src/views/fysp/config/components/CompInfoSearchFysp.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
<template>
  <CompInfoSearch
    label="监管用户"
    placeholder="输入用户名称"
    @search="searchSVUser"
  >
    <template #selected="{ row }">
      <div>
        <el-text>编号:{{ row?.svUserId }}</el-text>
      </div>
      <el-space>
        <el-text>名称:{{ row?.svUserName }}</el-text>
        <el-button
          v-show="row?.svUserName"
          type="primary"
          icon="DocumentCopy"
          text
          circle
          @click="copySVUser(row?.svUserName)"
        />
      </el-space>
    </template>
    <template #default="{ row, click }">
      <ItemUser :item="row" @add="selectSVUser(row, click)" />
    </template>
  </CompInfoSearch>
</template>
<script setup>
import CompInfoSearch from './CompInfoSearch.vue';
import { useCloned } from '@vueuse/core';
import svUserApi from '@/api/fysp/userApi';
const props = defineProps({
  // æ£€ç´¢èŒƒå›´ï¼ˆåŒ…含行政区划、场景类型)
  area: Object
});
// æŸ¥è¯¢ç›‘管用户
function searchSVUser(param, callback) {
  const { text, page, pageSize } = param;
  const { cloned: area } = useCloned(props.area);
  area.value.sceneName = text;
  return svUserApi
    .searchUser(area.value, text, page, pageSize)
    .then((res) => {
      if (res.success) {
        const l = res.data.map((value) => {
          return {
            ...value,
            district: value.remark
          };
        });
        callback({
          data: l,
          total: res.head.totalCount
        });
      }
    })
    .finally(() => {
      callback();
    });
}
function selectSVUser(row, click) {
  const p = {
    svUserId: row.guid,
    svUserName: row.realname,
    ...row
  };
  click(p);
}
</script>
src/views/fysp/config/device/CompDeviceMatchEdit.vue
@@ -94,7 +94,7 @@
<script>
import { useCloned } from '@vueuse/core';
import CompInfoSearch from './CompInfoSearch.vue';
import CompInfoSearch from '../components/CompInfoSearch.vue';
import tzUserApi from '@/api/fytz/userApi';
import svUserApi from '@/api/fysp/userApi';
import userMapApi from '@/api/fysp/userMapApi';
src/views/fysp/data-product/base-data-product/ProdEvaluationInfo.vue
@@ -1,4 +1,98 @@
<template>
  1
  <BaseProdProcess
    v-model:active="active"
    @onStep1="onStep1"
    @onStep2="onStep2"
    @onStep3="onStep3"
    :loading="loading"
  >
    <template #step2="{ contentHeight }">
      <el-table
        id="prod-evaluation-table"
        :data="tableData"
        v-loading="loading"
        :height="contentHeight + 'px'"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        size="small"
        border
      >
        <el-table-column fixed="left" prop="index" label="编号" width="50">
        </el-table-column>
        <el-table-column
          fixed="left"
          prop="subTask.scensename"
          label="名称"
          :show-overflow-tooltip="true"
          min-width="200"
        >
        </el-table-column>
        <el-table-column
          prop="subTask.planstarttime"
          label="巡查时间"
          :formatter="timeFormat"
          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="evaluate.townname" label="街道" width="80" />
        <el-table-column
          prop="evaluate.resultscorebef"
          label="评分"
          width="60"
        />
        <el-table-column prop="scoreLevel" label="规范性" width="70" />
        <el-table-column
          prop="evaluate.updatedate"
          label="更新时间"
          width="140"
          :formatter="timeFormat"
        />
      </el-table>
</template>
<script></script>
  </BaseProdProcess>
</template>
<script setup>
import { ref, inject } from 'vue';
import dayjs from 'dayjs';
import BaseProdProcess from '@/views/fysp/data-product/base-data-product/components/BaseProdProcess.vue';
import dataprodbaseApi from '@/api/fysp/dataprodbaseApi.js';
import { conversionFromTable } from '@/utils/excel';
import { useProdStepChange } from '@/views/fysp/data-product/prod-step-change.js';
const { active, changeActive } = useProdStepChange();
const loading = ref(false);
const tableData = ref([]);
function onStep1(opt) {
  loading.value = true;
  dataprodbaseApi
    .fetchProdEvaluateInfo(opt)
    .then((res) => {
      if (res.success) {
        tableData.value = res.data;
      }
      changeActive();
    })
    .finally(() => {
      loading.value = false;
    });
}
function onStep2() {
  changeActive();
}
function onStep3(val) {
  if (val.downloadType == '1') {
    loading.value = true;
    conversionFromTable('prod-evaluation-table', '规范性评估清单');
    loading.value = false;
  }
}
function timeFormat(row, column, cellValue, index) {
  return dayjs(cellValue).format('YYYY-MM-DD');
}
</script>
src/views/fysp/data-product/base-data-product/ProdInspectionInfo.vue
@@ -1,4 +1,104 @@
<template>
  1
  <BaseProdProcess
    v-model:active="active"
    @onStep1="onStep1"
    @onStep2="onStep2"
    @onStep3="onStep3"
    :loading="loading"
  >
    <template #step2="{ contentHeight }">
      <el-table
        id="prod-inspection-table"
        :data="tableData"
        v-loading="loading"
        :height="contentHeight + 'px'"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        size="small"
        border
      >
        <el-table-column fixed="left" prop="index" label="编号" width="50">
        </el-table-column>
        <el-table-column
          fixed="left"
          prop="subTask.scensename"
          label="名称"
          :show-overflow-tooltip="true"
          min-width="200"
        >
        </el-table-column>
        <el-table-column
          prop="subTask.planstarttime"
          label="巡查时间"
          :formatter="timeFormat"
          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="subTask.townname" label="街道" width="80" />
        <el-table-column
          prop="problems.length"
          label="问题数"
          width="60"
        />
        <el-table-column prop="scoreLevel" label="问题摘要" width="70" />
        <el-table-column
          prop="evaluate.resultscorebef"
          label="未整改数"
          width="60"
        />
        <el-table-column prop="scoreLevel" label="未整改问题" width="70" />
        <el-table-column
          prop="evaluate.updatedate"
          label="更新时间"
          width="140"
          :formatter="timeFormat"
        />
      </el-table>
</template>
<script></script>
  </BaseProdProcess>
</template>
<script setup>
import { ref, inject } from 'vue';
import dayjs from 'dayjs';
import BaseProdProcess from '@/views/fysp/data-product/base-data-product/components/BaseProdProcess.vue';
import dataprodbaseApi from '@/api/fysp/dataprodbaseApi.js';
import { conversionFromTable } from '@/utils/excel';
import { useProdStepChange } from '@/views/fysp/data-product/prod-step-change.js';
const { active, changeActive } = useProdStepChange();
const loading = ref(false);
const tableData = ref([]);
function onStep1(opt) {
  loading.value = true;
  dataprodbaseApi
    .fetchProdInspectionInfo(opt)
    .then((res) => {
      if (res.success) {
        tableData.value = res.data;
      }
      changeActive();
    })
    .finally(() => {
      loading.value = false;
    });
}
function onStep2() {
  changeActive();
}
function onStep3(val) {
  if (val.downloadType == '1') {
    loading.value = true;
    conversionFromTable('prod-inspection-table', '整改清单');
    loading.value = false;
  }
}
function timeFormat(row, column, cellValue, index) {
  return dayjs(cellValue).format('YYYY-MM-DD');
}
</script>
src/views/fysp/data-product/base-data-product/ProdManage.vue
@@ -5,7 +5,6 @@
      default-active="scene"
      ellipsis
      mode="horizontal"
      style="max-width: 600px; background-color: aliceblue"
    >
      <el-menu-item
        v-for="item in menu"
@@ -16,7 +15,7 @@
      >
    </el-menu>
  </el-affix>
  <router-view v-slot="{ Component, route }" :style="'height: ' + height">
  <router-view v-slot="{ Component, route }" :style="{ height: height + 'px' }">
    <keep-alive>
      <component
        v-if="route.meta.keepAlive"
@@ -28,14 +27,16 @@
  </router-view>
</template>
<script setup>
import { ref, onMounted, provide } from 'vue';
import { ref, onMounted, provide, inject, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
const contentMaxHeight = inject('contentMaxHeight');
const router = useRouter();
const route = useRoute();
const menuRef = ref(null);
const height = ref('calc(100vh - 64px)');
const height = ref(contentMaxHeight.value);
const menu = ref([
  {
@@ -48,7 +49,7 @@
    path: 'evaluate'
  },
  {
    name: '巡查信息',
    name: '整改清单',
    path: 'inspection'
  },
  {
@@ -67,7 +68,8 @@
function calcTableHeight() {
  const h = menuRef.value.$el.offsetHeight;
  return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
  return contentMaxHeight.value - h;
  // return `calc(100vh - ${h}px - 60px - var(--el-main-padding) * 2)`;
}
onMounted(() => {
@@ -75,6 +77,9 @@
});
// æä¾›ç»™å†…部组件视图最大高度
provide('viewHeight', height);
provide(
  'viewHeight',
  computed(() => height.value)
);
</script>
<style scoped></style>
src/views/fysp/data-product/base-data-product/ProdSceneInfo.vue
@@ -2,6 +2,8 @@
  <BaseProdProcess
    v-model:active="active"
    @onStep1="onStep1"
    @onStep2="onStep2"
    @onStep3="onStep3"
    :loading="loading"
  >
    <!-- <template #step1>
@@ -9,15 +11,16 @@
    </template> -->
    <template #step2="{ contentHeight }">
      <el-table
        id="prod-scene-table"
        :data="tableData"
        v-loading="loading"
        :height="viewHeight"
        :height="contentHeight + 'px'"
        table-layout="fixed"
        :show-overflow-tooltip="true"
        size="small"
        border
      >
        <el-table-column fixed="left" prop="index" label="编号" width="40">
        <el-table-column fixed="left" prop="index" label="编号" width="50">
        </el-table-column>
        <el-table-column
          fixed="left"
@@ -35,6 +38,8 @@
        <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="contacts" label="联系人" width="70" />
        <el-table-column prop="contactst" label="电话" width="96" />
        <!-- <el-table-column prop="longitude" label="经度" width="110" />
        <el-table-column prop="latitude" label="纬度" width="110" /> -->
        <!-- <el-table-column
@@ -54,16 +59,12 @@
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';
import { conversionFromTable } from '@/utils/excel';
import { useProdStepChange } from '@/views/fysp/data-product/prod-step-change.js';
const active = ref(1);
const { active, changeActive } = useProdStepChange();
const loading = ref(false);
const tableData = ref([]);
const viewHeight = inject('viewHeight');
function changeActive() {
  active.value++;
  active.value = active.value > 3 ? 1 : active.value;
}
function onStep1(opt) {
  loading.value = true;
@@ -84,6 +85,18 @@
    });
}
function onStep2() {
  changeActive();
}
function onStep3(val) {
  if (val.downloadType == '1') {
    loading.value = true;
    conversionFromTable('prod-scene-table', '巡查场景清单');
    loading.value = false;
  }
}
function timeFormat(row, column, cellValue, index) {
  return dayjs(cellValue).format('YYYY-MM-DD HH:mm:ss');
}
src/views/fysp/data-product/base-data-product/components/BaseProdProcess.vue
@@ -22,7 +22,7 @@
        <div
          v-show="showStep1Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight }"
          :style="{ height: viewHeight + 'px' }"
          @click="changeActive(1)"
        >
          <div class="prod-thumbnail">①修改选项</div>
@@ -38,6 +38,9 @@
        <div v-show="showStep2Content">
          <div ref="titleRef" class="prod-title">
            <el-text tag="b" size="large">数据产品预览</el-text>
            <el-button type="primary" @click="$emit('onStep2')">
              ä¸‹è½½æ•°æ®äº§å“
            </el-button>
          </div>
          <slot name="step2" :contentHeight="contentHeight"></slot>
        </div>
@@ -49,7 +52,7 @@
        <div
          v-show="showStep2Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight }"
          :style="{ height: viewHeight + 'px' }"
          @click="changeActive(2)"
        >
          <div
@@ -74,7 +77,11 @@
            <slot name="step3"></slot>
          </template>
          <template v-else>
            <ProdDownload></ProdDownload>
            <ProdDownload
              :loading="loading"
              :queryOpt="queryOpt"
              @submit="onDownload"
            ></ProdDownload>
          </template>
        </div>
      </transition>
@@ -85,7 +92,7 @@
        <div
          v-show="showStep3Thumbnail"
          class="prod-thumbnail-wrapper"
          :style="{ height: viewHeight }"
          :style="{ height: viewHeight + 'px' }"
          @click="changeActive(3)"
        >
          <div
@@ -102,7 +109,7 @@
  </el-row>
</template>
<script setup>
import { computed, inject, ref, watch, onMounted } from 'vue';
import { computed, inject, ref, watch, onMounted, onUnmounted } 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';
@@ -118,9 +125,10 @@
  }
});
const emit = defineEmits(['update:active', 'onStep1']);
const emit = defineEmits(['update:active', 'onStep1', 'onStep2', 'onStep3']);
const viewHeight = inject('viewHeight');
const contentMaxHeight = inject('contentMaxHeight');
const viewHeight = inject('viewHeight', contentMaxHeight.value);
const btnDisabled = ref(false);
@@ -128,13 +136,13 @@
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);
  // console.log(titleRef.value.offsetHeight);
  contentHeight.value = viewHeight.value - (titleRef.value?.offsetHeight || 0);
  // console.log(contentHeight.value);
}
// æ•°æ®äº§å“ç”Ÿæˆé€‰é¡¹
const queryOpt = ref({});
// æ­¥éª¤å¼•用
const step1Ref = ref(null);
@@ -217,7 +225,11 @@
}
function onSearch(opt) {
  queryOpt.value = opt;
  emit('onStep1', opt);
}
function onDownload(val) {
  emit('onStep3', val);
}
function changeActive(index) {
  let isAnimate = false;
@@ -234,8 +246,21 @@
  // emit('update:active', index);
}
let resizeObserver = null;
onMounted(() => {
  if (titleRef.value) {
    resizeObserver = new ResizeObserver(() => {
  calContentHeight();
    });
    resizeObserver.observe(titleRef.value);
  }
});
// åœ¨ç»„件卸载时清理
onUnmounted(() => {
  if (resizeObserver && titleRef.value) {
    resizeObserver.unobserve(titleRef);
  }
});
</script>
<style scoped>
@@ -263,7 +288,10 @@
}
.prod-title {
  padding: 10px;
  padding: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.prod-thumbnail-wrapper {
@@ -285,7 +313,7 @@
  text-orientation: upright;
  letter-spacing: 8px;
  font-size: 18px;
  font-weight: 500;
  font-weight: 600;
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
  cursor: pointer;
src/views/fysp/data-product/base-data-product/components/ProdDownload.vue
@@ -1,2 +1,68 @@
<template>ProdDownload</template>
<script setup></script>
<template>
  <el-card shadow="never">
    <template #header>
      <div><el-text tag="b" size="large">数据产品下载</el-text></div>
    </template>
    <el-form :inline="false" label-position="left" label-width="150px">
      <el-form-item label="区县">
        <el-text>{{ queryOpt.districtName }}</el-text>
      </el-form-item>
      <el-form-item label="时间范围">
        <el-text>{{ queryOpt.startTime }} è‡³ {{ queryOpt.endTime }}</el-text>
      </el-form-item>
      <el-form-item label="场景类型">
        <el-text>{{ queryOpt.sceneTypeName }}</el-text>
      </el-form-item>
      <el-form-item label="产品形式">
        <el-radio-group v-model="downloadType">
          <el-radio value="1"> Excel表单 </el-radio>
          <el-radio value="2" :disabled="true"> Word文档 </el-radio>
        </el-radio-group>
      </el-form-item>
    </el-form>
    <template #footer>
      <el-row justify="end">
        <el-button
          type="primary"
          size="default"
          :loading="loading"
          @click="submit"
          icon="Download"
          >下载</el-button
        >
      </el-row>
    </template>
  </el-card>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
import scene_1 from '@/assets/image/scene_1.png';
const props = defineProps({
  // æ•°æ®äº§å“ç”Ÿæˆé€‰é¡¹
  queryOpt: {
    type: Object,
    default: () => {}
  },
  loading: {
    type: Boolean,
    default: false
  }
});
const emit = defineEmits(['submit']);
const downloadType = ref('1');
const submit = () => {
  emit('submit', {
    downloadType: downloadType.value
  });
};
</script>
<style scoped>
/* .image {
  width: 200px;
  height: 200px;
} */
</style>
src/views/fysp/data-product/base-data-product/components/ProdQueryOpt.vue
@@ -12,7 +12,7 @@
    >
    </SearchBar>
    <template #footer>
      <el-row v-show="active" justify="space-around">
      <el-row v-show="active" justify="end">
        <el-button
          type="primary"
          size="default"
@@ -49,16 +49,22 @@
const search = (options) => {
  const opt = {
    topTaskId: options.topTask.tguid,
    topTaskName: options.topTask.name,
    provinceCode: options.topTask.provincecode,
    provinceName: options.topTask.provincename,
    cityCode: options.topTask.citycode,
    cityName: options.topTask.cityname,
    districtCode: options.topTask.districtcode,
    districtName: options.topTask.districtname,
    townCode: options.topTask.towncode,
    townName: options.topTask.townname,
    startTime: dayjs(options.topTask.starttime).format('YYYY-MM-DD HH:mm:ss'),
    endTime: dayjs(options.topTask.endtime)
      .add(1, 'day')
      .add(-1, 'second')
      .format('YYYY-MM-DD HH:mm:ss'),
    sceneTypeId: options.sceneTypeId,
    sceneTypeName: options.sceneTypeName,
    needCache: true
  };
  emit('submit', opt);
src/views/fysp/data-product/prod-step-change.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
import { ref } from 'vue';
/**
 * æ•°æ®äº§å“æ­¥éª¤åˆ‡æ¢
 */
export function useProdStepChange() {
  const active = ref(1);
  function changeActive() {
    active.value++;
    active.value = active.value > 3 ? 1 : active.value;
  }
  return {
    active,
    changeActive
  };
}
src/views/fysp/support/JingAnNightConstruction.vue
@@ -1,9 +1,12 @@
<template>
  <FYTable @search="onSearch">
    <template #options> </template>
    <template #buttons> </template>
  <FYTable
    :data="data"
    :total-count="total"
    @search="onSearch"
    :extraHeight="tabsHeaderHeight"
  >
    <!-- <template #options> </template>
    <template #buttons> </template> -->
    <template #table-column>
      <el-table-column
        fixed="left"
@@ -48,40 +51,72 @@
          {{ $fm.formatYMD(row.ncCreateTime) }}
        </template>
      </el-table-column> -->
      <el-table-column prop="ncUserId" label="匹配用户" width="110">
      <el-table-column prop="ncUserId" label="匹配用户">
        <template #default="{ row }">
          <el-text v-loading="row._loading">{{
            row._user ? row._user.realName : '未匹配'
          }}</el-text>
          <el-text
            :loading="row._loading"
            :type="row._user ? 'primary' : 'danger'"
            >{{ row._user ? row._user.realname : '未匹配' }}</el-text
          >
        </template>
      </el-table-column>
      <el-table-column fixed="right" label="操作" width="160">
        <template #default="scope">
      <el-table-column fixed="right" label="操作" width="80">
        <template #default="{ row }">
          <el-button
            :loading="scope.row.loading1"
            :disabled="row._loading"
            type="default"
            size="small"
            @click="itemEdit(scope)"
            @click="itemEdit(row)"
            >编辑</el-button
          >
          <el-button
            :loading="scope.row.loading2"
            :type="scope.row.extension1 != '0' ? 'danger' : 'primary'"
            size="small"
            @click="itemActive(scope)"
            >{{ scope.row.extension1 != '0' ? '下线' : '上线' }}</el-button
          >
        </template>
      </el-table-column>
    </template>
  </FYTable>
  <el-dialog v-model="dialog" destroy-on-close>
    <CompInfoSearchFysp
      v-model="selectedSVUser"
      :area="area"
      :defaultText="defaultText"
      searchTextWidth="400px"
    />
    <template #footer>
      <el-button @click="dialog = false">取消</el-button>
      <el-button type="primary" @click="submit" :disabled="!selectedSVUser"
        >确定</el-button
      >
    </template>
  </el-dialog>
</template>
<script setup>
import { ref } from 'vue';
import { ref, inject, computed } from 'vue';
import nightConstructionApi from '@/api/fysp/nightConstructionApi';
import userApi from '@/api/fysp/userApi';
import CompInfoSearchFysp from '@/views/fysp/config/components/CompInfoSearchFysp.vue';
import { ElMessage } from 'element-plus';
const tabsHeaderHeight = inject('tabsHeaderHeight', 0);
// å¤œé—´æ–½å·¥è®°å½•及总数
const data = ref([]);
const total = ref(0);
// å¤œé—´æ–½å·¥è®°å½•匹配弹窗
const dialog = ref(false);
const selectedSVUser = ref(null);
const selectedRow = ref(null);
const area = ref({
  provincecode: '31',
  provincename: '上海市',
  citycode: '3100',
  cityname: '上海市',
  districtcode: '310106',
  districtname: '静安区',
  scensetypeid: '1'
});
const defaultText = computed(() => {
  return selectedRow.value?.ncItemName || undefined;
});
function onSearch(page, callback) {
  return nightConstructionApi
@@ -93,17 +128,61 @@
    })
    .then((res) => {
      if (res.success) {
        res.data.forEach((d) => {
          res.data._loading = true;
          userApi.getUserById(d.ncUserId).then((res1) => {
            res.data._user = res1;
        data.value = res.data;
        data.value.forEach((d) => {
          d._loading = true;
          if (d.ncUserId) {
            userApi
              .getUserById(d.ncUserId)
              .then((res1) => {
                d._user = res1;
              })
              .finally(() => {
                d._loading = false;
          });
          } else {
            d._loading = false;
          }
        });
        callback({
          data: res.data,
          total: res.head.totalCount
        total.value = res.head.totalCount;
        callback();
      }
        });
      }
function itemEdit(row) {
  selectedRow.value = row;
  selectedSVUser.value = row._user;
  dialog.value = true;
}
function submit() {
  if (!selectedSVUser.value) {
    return ElMessage.error('请选择用户');
  }
  nightConstructionApi
    .updateRecord({
      recordId: selectedRow.value.ncId,
      userId: selectedSVUser.value.guid,
      sceneId: selectedSVUser.value.dguid
    })
    .then((res) => {
      if (res.success) {
        selectedRow.value.ncUserId = res.data.ncUserId;
        selectedRow.value.ncSceneId = res.data.ncSceneId;
        userApi
          .getUserById(selectedRow.value.ncUserId)
          .then((res1) => {
            selectedRow.value._user = res1;
          })
          .finally(() => {
            selectedRow.value._loading = false;
          });
      }
    })
    .finally(() => {
      dialog.value = false;
      selectedRow.value._user = selectedSVUser.value;
    });
}
</script>
src/views/fysp/support/JingAnSupport.vue
@@ -1,5 +1,5 @@
<template>
  <el-tabs type="border-card">
  <el-tabs ref="tabsRef">
    <el-tab-pane label="静安夜间施工管理">
      <JingAnNightConstruction></JingAnNightConstruction>
    </el-tab-pane>
@@ -12,8 +12,52 @@
  </el-tabs>
</template>
<script setup>
import { ref, onMounted, provide, computed } from 'vue';
import NewDevice from './NewDevice.vue';
import NewConstruction from './NewConstruction.vue';
import JingAnNightConstruction from './JingAnNightConstruction.vue';
// å®šä¹‰ tabs ref
const tabsRef = ref(null);
const tabsHeaderHeight = ref(0);
onMounted(() => {
  // ç¡®ä¿ DOM å·²ç»æ¸²æŸ“完成
  setTimeout(() => {
    tabsHeaderHeight.value = getTabsHeaderHeight();
  }, 0);
});
function getTabsHeaderHeight() {
  if (tabsRef.value) {
    // èŽ·å– el-tabs ç»„ä»¶çš„ DOM å…ƒç´ 
    const tabsElement = tabsRef.value.$el;
    // Element UI çš„ el-tabs header é€šå¸¸æœ‰ .el-tabs__header ç±»å
    const headerElement = tabsElement.querySelector('.el-tabs__header');
    if (headerElement) {
      // èŽ·å– header çš„ offsetHeight(包含 padding å’Œ border,不包含 margin)
      const offsetHeight = headerElement.offsetHeight;
      // èŽ·å–è®¡ç®—æ ·å¼ä»¥èŽ·å– margin å€¼
      const computedStyle = window.getComputedStyle(headerElement);
      // è§£æž margin å€¼ï¼ˆä¸Šä¸‹å·¦å³ï¼‰
      const marginTop = parseFloat(computedStyle.marginTop || 0);
      const marginBottom = parseFloat(computedStyle.marginBottom || 0);
      // const marginLeft = parseFloat(computedStyle.marginLeft || 0);
      // const marginRight = parseFloat(computedStyle.marginRight || 0);
      // è®¡ç®—总高度(包含所有 padding、border å’Œ margin)
      const totalHeightWithMargin = offsetHeight + marginTop + marginBottom;
      return totalHeightWithMargin;
    }
  }
  return 0;
}
provide('tabsHeaderHeight', computed(() => tabsHeaderHeight.value));
</script>
<style scoped></style>