riku
2023-10-31 2d3d56ff801b73afdb779267004d740f9beafe57
2023.10.31
已修改25个文件
已删除4个文件
已添加15个文件
已重命名3个文件
2278 ■■■■■ 文件已修改
src/api/clue/clueApi.js 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueConclusionApi.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueQuestionApi.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/index.js 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/config.js 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/grid/gridInfoApi.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/grid/gridSchemeApi.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/gridRecordApi.js 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/base.css 70 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/layout.css 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/main.css 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/shortcut.css 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/text.css 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/button/CloseButton.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/core/CoreHeader.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/list/DescriptionsList.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/list/DescriptionsListItem.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/baseMapUtil.js 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/mapGrid.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/search-option/OptionTime.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/formConfirm.js 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/messageBox.js 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/model/clueQuestion.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/model/gridRecord.js 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/grid.js 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/textFormat.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/HomePage.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/ClueLayout.vue 115 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/ClueList.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/ClueReport.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/list/ClueManage.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/list/components/ClueList.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/ClueReport.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/ClueReportClue.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/ClueReportConclusion.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/ClueReportQuestion.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/QuestionDetail.vue 84 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-grid/GridLayout.vue 99 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-grid/components/GridCreate.vue 96 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-grid/components/GridEditing.vue 94 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-grid/components/ListGridDetail.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-grid/components/OptionGridRecord.vue 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-grid/components/SchemeCreate.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueApi.js
@@ -1,23 +1,43 @@
import { $clue } from './index';
import { $clue } from '../index';
export default {
  /**
   * æŸ¥è¯¢çº¿ç´¢æ¸…单
   * @param {object} param0
   * @returns
   * @param {object} param0
   * @returns
   */
  getClue({ sTime, eTime, pageNum = 1, pageSize = 30 }) {
    let url = 'clue/fetch?';
    if (sTime) {
      url += `sTime=${sTime}&`;
    }
    if (eTime) {
      url += `eTime=${eTime}&`;
    }
    return $clue.get(
      `clue/fetch?sTime=${sTime}&eTime=${eTime}&pageNum=${pageNum}&pageSize=${pageSize}`
      `${url}pageNum=${pageNum}&pageSize=${pageSize}`
    );
  },
  /**
   * ä»Žç¬¬ä¸‰æ–¹è¿œç¨‹æ‹‰å–线索清单
   * @param {string} updateTime æ›´æ–°æ—¶é—´ï¼ŒèŽ·å–è¯¥æ—¶é—´ä¹‹åŽçš„çº¿ç´¢
   * @returns
   * @returns
   */
  fetchRemoteClue(updateTime) {
    return $clue.get(`clue/fetch/remote?updateTime=${updateTime}`);
  },
  fetchRemoteClueFileUrl(clueId) {
    return `${$clue.defaults.baseURL}clue/fetch/remote/file?clueId=${clueId}`;
  },
  /**
   * æŽ¨é€çº¿ç´¢çš„结论与问题
   * @param {string} clueId
   * @returns
   */
  pushClue(clueId) {
    return $clue.post(`clue/push?clueId=${clueId}`);
  }
};
src/api/clue/clueConclusionApi.js
@@ -1,4 +1,4 @@
import { $clue } from './index';
import { $clue } from '../index';
export default {
  /**
src/api/clue/clueQuestionApi.js
@@ -1,4 +1,4 @@
import { $clue } from './index';
import { $clue } from '../index';
import { getClueQuestionList } from '@/model/clueQuestion';
export default {
src/api/clue/index.js
ÎļþÒÑɾ³ý
src/api/config.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
import { ElMessage } from 'element-plus';
/**
 * è®¾ç½®ç½‘路请求监听
 */
function setInterceptors(...instance) {
  instance.forEach((i) => {
    // æ·»åŠ è¯·æ±‚æ‹¦æˆªå™¨
    i.interceptors.request.use(
      function (config) {
        // åœ¨å‘送请求之前, æ·»åŠ è¯·æ±‚å¤´
        // config.headers = addHeaders(config.headers);
        console.log('==>请求开始');
        console.log(`${config.baseURL}${config.url}`);
        if (config.data) {
          console.log('==>请求数据', config.data);
        }
        return config;
      },
      function (error) {
        // å¯¹è¯·æ±‚错误做些什么
        console.log('==>请求开始');
        console.log(error);
        ElMessage({
          message: error,
          type: 'error'
        });
        return Promise.reject(error);
      }
    );
    // æ·»åŠ å“åº”æ‹¦æˆªå™¨
    i.interceptors.response.use(
      function (response) {
        // 2xx èŒƒå›´å†…的状态码都会触发该函数。
        // å¯¹å“åº”数据做点什么
        console.log(response);
        console.log('==>请求结束');
        if (response.status == 200) {
          if (
            response.data.success != undefined &&
            response.data.success != null
          ) {
            if (response.data.success == true) {
              // if (response.data.message && response.data.message != '') {
              //   ElMessage({
              //     message: response.data.message,
              //     type: 'success'
              //   });
              // }
              return response.data.data;
            } else {
              ElMessage({
                message: response.data.message,
                type: 'error'
              });
              return Promise.reject(response.data.message);
            }
          } else {
            return response;
          }
        } else {
          return Promise.reject(response);
        }
      },
      function (error) {
        // è¶…出 2xx èŒƒå›´çš„状态码都会触发该函数。
        // å¯¹å“åº”错误做点什么
        console.log(error);
        console.log('==>请求结束');
        ElMessage({
          message: error,
          type: 'error'
        });
        return Promise.reject(error);
      }
    );
  });
}
export { setInterceptors };
src/api/grid/gridInfoApi.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
import { $clue } from '../index';
import { getGridRecord, getGridRecordList } from '@/model/gridRecord';
export default {
  /**
   * èŽ·å–æ–¹æ¡ˆç½‘æ ¼ä¿¡æ¯
   */
  fetchGridList(schemeId) {
    return $clue.get(`grid/info/fetch?id=${schemeId}`).then((res) => {
      return getGridRecordList(res);
    });
  },
  /**
   * æ–°å»ºç½‘æ ¼
   * @param {Object} grid
   */
  createGrid(gridInfo) {
    return $clue.post(`grid/info/create`, gridInfo).then((res) => {
      return getGridRecord(res);
    });
  },
  /**
   * æ›´æ–°ç½‘æ ¼
   * @param {Object} gridInfo
   */
  updateGrid(gridInfo) {
    return $clue.post(`grid/info/update`, gridInfo)
  }
};
src/api/grid/gridSchemeApi.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import { $clue } from '../index';
export default {
  /**
   * èŽ·å–å…¨éƒ¨ç½‘æ ¼åŒ–è§„åˆ’æ–¹æ¡ˆ
   */
  fetchAllSchemes() {
    return $clue.get(`grid/scheme/fetch`);
  },
  /**
   * æ–°å»ºç½‘格方案
   * @param {Object} scheme
   */
  createScheme(scheme) {
    return $clue.post(`grid/scheme/create`, scheme);
  }
};
src/api/gridRecordApi.js
ÎļþÒÑɾ³ý
src/api/index.js
@@ -0,0 +1,33 @@
import axios from 'axios';
import { setInterceptors } from "./config";
const url = 'http://47.100.191.150:9031/';
// const url = 'http://192.168.1.9:8080/';
const imgUrl = 'http://47.100.191.150:9031/images/';
//飞羽监管
const $clue = axios.create({
  baseURL: url,
  timeout: 10000
  // headers: addHeaders()
});
// function getHeaders() {
//   const token = 'e6dc8bb9e1ff0ce973fb92b4af2e4c3f';
//   const date = new Date();
//   const timestamp = parseInt(date.getTime() / 1000) - 200;
//   const sign = md5(timestamp + token);
//   return {
//     'JA-TIMESTAMP': timestamp,
//     'JA-SIGN': sign,
//     'JA-TOKEN': token
//   };
// }
//添加拦截器
setInterceptors($clue)
export { $clue, imgUrl };
src/assets/base.css
@@ -1,55 +1,3 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
  --vt-c-white: #ffffff;
  --vt-c-white-soft: #f8f8f8;
  --vt-c-white-mute: #f2f2f2;
  --vt-c-black: #181818;
  --vt-c-black-soft: #222222;
  --vt-c-black-mute: #282828;
  --vt-c-indigo: #2c3e50;
  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
  --vt-c-text-light-1: var(--vt-c-indigo);
  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
  --vt-c-text-dark-1: var(--vt-c-white);
  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
  --color-background: var(--vt-c-white);
  --color-background-soft: var(--vt-c-white-soft);
  --color-background-mute: var(--vt-c-white-mute);
  --color-border: var(--vt-c-divider-light-2);
  --color-border-hover: var(--vt-c-divider-light-1);
  --color-heading: var(--vt-c-text-light-1);
  --color-text: var(--vt-c-text-light-1);
  --section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
  :root {
    --color-background: var(--vt-c-black);
    --color-background-soft: var(--vt-c-black-soft);
    --color-background-mute: var(--vt-c-black-mute);
    --color-border: var(--vt-c-divider-dark-2);
    --color-border-hover: var(--vt-c-divider-dark-1);
    --color-heading: var(--vt-c-text-dark-1);
    --color-text: var(--vt-c-text-dark-2);
  }
}
*,
*::before,
*::after {
@@ -58,9 +6,23 @@
  font-weight: normal;
}
:root {
  --fy-head-height: 50px;
  --fy-body-height: calc(100% - var(--fy-head-height));
}
table {
  border-collapse: collapse;
  border-spacing: 0;
}
td, th {
  padding: 0;
}
body {
  --screen-min-width: 1440px;
  --screen-min-height: 900px;
  --screen-min-width: 1200px;
  --screen-min-height: 600px;
  min-height: var(--screen-min-height);
  min-width: var(--screen-min-width);
  /* overflow: scroll; */
src/assets/layout.css
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
.fy-head {
  height: var(--fy-head-height);
}
.fy-body {
  height: var(--fy-body-height);
}
.fy-overlay-container {
  pointer-events: none;
  /* background-color: aqua; */
}
.fy-card {
  position: relative;
  /* height: 700px; */
  background: white;
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  pointer-events: auto;
  box-shadow: var(--el-box-shadow-dark);
  /* padding: 0 8px; */
}
.fy-main {
  /* background-color: aliceblue; */
  padding: 8px 8px 16px 8px;
  font-size: var(--el-font-size-base);
}
.fy-main-border {
  /* background-color: aliceblue; */
  padding: 0 8px;
  font-size: var(--el-font-size-base);
  border: var(--el-border);
  border-radius: 6px;
}
.fy-column-reverse {
  display: flex;
  flex-direction: column-reverse;
  height: 100%;
}
.fy-flex-row {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 0 8px;
}
.fy-flex-row>span{
  color: var(--el-text-color-regular);
}
src/assets/main.css
@@ -1,34 +1,10 @@
@import './base.css';
@import './border.css';
@import './text.css';
@import './layout.css';
@import './shortcut.css';
#app {
  margin: 0 auto;
  font-weight: normal;
}
/* a,
.green {
  text-decoration: none;
  color: hsla(160, 100%, 37%, 1);
  transition: 0.4s;
} */
@media (hover: hover) {
  /* a:hover {
    background-color: hsla(160, 100%, 37%, 0.2);
  } */
}
@media (min-width: 1024px) {
  /* body {
    display: flex;
    place-items: center;
  }
  #app {
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 0 2rem;
  } */
}
src/assets/shortcut.css
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
.flex {
  display: flex;
}
.flex-col {
  display: flex;
  flex-direction: column;
}
.gap-1 {
  gap: 4px;
}
.p-h-1 {
  padding: 0 8px;
}
src/assets/text.css
@@ -1,14 +1,16 @@
.fy-h1 {
  padding: 8px 8px 8px 8px;
  font-size: var(--el-font-size-large);
  font-weight: 600;
}
.fy-h2 {
  padding: 16px 0 8px 0;
  padding: 8px 8px;
  font-size: var(--el-font-size-medium);
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 600;
}
.fy-p1 {
src/components.d.ts
@@ -8,26 +8,32 @@
declare module 'vue' {
  export interface GlobalComponents {
    BaseMap: typeof import('./components/map/BaseMap.vue')['default']
    CloseButton: typeof import('./components/button/CloseButton.vue')['default']
    CoreHeader: typeof import('./components/core/CoreHeader.vue')['default']
    DescriptionsList: typeof import('./components/list/DescriptionsList.vue')['default']
    DescriptionsListItem: typeof import('./components/list/DescriptionsListItem.vue')['default']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDivider: typeof import('element-plus/es')['ElDivider']
    ElEmpty: typeof import('element-plus/es')['ElEmpty']
    ElForm: typeof import('element-plus/es')['ElForm']
    ElFormItem: typeof import('element-plus/es')['ElFormItem']
    ElIcon: typeof import('element-plus/es')['ElIcon']
    ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElOption: typeof import('element-plus/es')['ElOption']
    ElRadio: typeof import('element-plus/es')['ElRadio']
    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    MapSearch: typeof import('./components/map/MapSearch.vue')['default']
    OptionTime: typeof import('./components/search-option/OptionTime.vue')['default']
src/components/button/CloseButton.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
<template>
  <div class="wrapper">
    <el-button
      class="close-btn"
      type="danger"
      icon="Close"
      circle
      @click="close"
    />
    <slot></slot>
  </div>
</template>
<script>
export default {
  emits: ['close'],
  methods: {
    close() {
      this.$emit('close');
    }
  }
};
</script>
<style scoped>
.wrapper {
  position: relative;
  padding-right: 10px;
  pointer-events: auto;
}
.close-btn {
  position: absolute;
  right: 2px;
  top: -10px;
  z-index: 1;
}
</style>
src/components/core/CoreHeader.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
<template>
  <el-row class="fy-head">
    <div>
      <el-radio-group
        class="container"
        v-model="radio1"
        size="large"
        @change="onChange"
      >
        <el-radio-button
          v-for="item in radioOptions"
          :key="item.label"
          :label="item.label"
          >{{ item.name }}</el-radio-button
        >
      </el-radio-group>
    </div>
  </el-row>
</template>
<script>
export default {
  emits: ['onChange'],
  data() {
    return {
      radioOptions: [
        { name: '线索管理', label: 0 },
        { name: '网格管理', label: 1 }
      ],
      radio1: 0
    };
  },
  methods: {
    onChange(e) {
      this.$emit('onChange', e);
    }
  }
};
</script>
<style scoped>
.container {
  pointer-events: auto;
}
</style>
src/components/list/DescriptionsList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
<template>
  <div class="title-wrapper">
    <div v-if="title" class="fy-h2">{{ title }}</div>
    <slot name="extra"></slot>
  </div>
  <table>
    <tbody>
      <slot></slot>
    </tbody>
  </table>
</template>
<script>
export default {
  props: {
    title: String
  }
};
</script>
<style scoped>
.title-wrapper {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
table {
  width: 100%;
}
</style>
src/components/list/DescriptionsListItem.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
<template>
  <tr>
    <td v-if="label" class="td-1">{{ label }}</td>
    <td v-else class="td-1"><slot name="label"></slot></td>
    <td v-if="content" class="td-2">{{ content }}</td>
    <td v-else class="td-2"><slot name="content"></slot></td>
  </tr>
</template>
<script>
export default {
  props: {
    label: String,
    content: String
  }
};
</script>
<style scoped>
tr {
  font-size: var(--el-font-size-small);
}
td {
  border: var(--el-border);
  padding: 2px 6px;
}
.td-1 {
  width: 68px;
  background-color: var(--el-fill-color-light);
  color: var(--el-text-color-regular);
}
.td-2 {
  color: var(--el-text-color-primary);
}
</style>
src/components/map/baseMapUtil.js
@@ -80,21 +80,31 @@
    });
  },
  addMarker(lnglat) {
    const marker = new AMap.Marker({
      position: lnglat
    });
    map.add(marker);
    this.setCenter(lnglat);
    return marker;
  },
  setCenter(lnglat) {
    map.setCenter(lnglat);
  },
  /**
   * ç¼©æ”¾åœ°å›¾åˆ°åˆé€‚的视野级别
   */
  setFitView(overlays, type = 0) {
  setFitView(...overlays) {
    const _overlays = toRaw(overlays);
    switch (type) {
      case 0:
        map.setFitView([_overlays]);
        break;
      case 1:
        map.setFitView(_overlays);
        break;
      default:
        map.setFitView([_overlays]);
        break;
    map.setFitView(_overlays, true, [60, 60, 500, 60], 14.5);
  },
  addView(overlays) {
    if (overlays) {
      const _overlays = toRaw(overlays);
      map.add(_overlays);
    }
  },
@@ -102,8 +112,10 @@
   * ç§»é™¤è¦†ç›–物
   */
  removeView(overlays) {
    const _overlays = toRaw(overlays);
    map.remove(_overlays);
    if (overlays) {
      const _overlays = toRaw(overlays);
      map.remove(_overlays);
    }
  },
  /**
@@ -133,8 +145,10 @@
        (((a * (1 - ee)) / (magic * sqrtmagic)) * PI);
      dlng =
        (dlng * 180.0) / ((a / sqrtmagic) * Math.cos(radlat) * PI);
      let mglat = Math.round((lat * 2 - lat - dlat) * 1000000) / 1000000;
      let mglng = Math.round((lng * 2 - lng - dlng) * 1000000) / 1000000;
      let mglat =
        Math.round((lat * 2 - lat - dlat) * 1000000) / 1000000;
      let mglng =
        Math.round((lng * 2 - lng - dlng) * 1000000) / 1000000;
      return [mglng, mglat];
    }
  },
@@ -164,5 +178,16 @@
      let mglng = Math.round((lng + dlng) * 1000000) / 1000000;
      return [mglng, mglat];
    }
  },
  gpsConvert(gps) {
    return new Promise((reject) => {
      // å‚数说明:需要转换的坐标,需要转换的坐标类型,转换成功后的回调函数
      AMap.convertFrom(gps, 'baidu', function (status, result) {
        if (result.info === 'ok') {
          var lnglats = result.locations; // è½¬æ¢åŽçš„高德坐标 Array.<LngLat>
          reject(lnglats[0]);
        }
      });
    });
  }
};
src/components/map/mapGrid.js
@@ -37,10 +37,10 @@
    };
    mouseTool.on('draw', lastDrawEvent);
    mouseTool.polygon({
      strokeColor: '#FF33FF',
      strokeColor: 'green',
      strokeOpacity: 1,
      strokeWeight: 6,
      fillColor: '#1791fc',
      strokeWeight: 2,
      fillColor: '#fff',
      fillOpacity: 0.4,
      // çº¿æ ·å¼è¿˜æ”¯æŒ 'dashed'
      strokeStyle: 'solid'
src/components/search-option/OptionTime.vue
@@ -9,7 +9,7 @@
<script>
// ç½‘格化方案记录选项
import gridRecordApi from '@/api/gridRecordApi';
import gridSchemeApi from '@/api/grid/gridSchemeApi';
export default {
  props: {
@@ -37,7 +37,7 @@
  },
  methods: {
    getOptions() {
      gridRecordApi.getGridRecords().then((res) => {
      gridSchemeApi.getGridRecords().then((res) => {
        this.options = res;
        this.selectedOptions = res[0];
      });
src/composables/formConfirm.js
@@ -75,9 +75,10 @@
  // æ¸…空表单
  const clear = function () {
    formRef.value.clearValidate();
    edit.value = false;
    isReset = true;
    formObj.value = {}
    formObj.value = {};
  };
  // æäº¤æˆåŠŸåŽ
@@ -94,9 +95,9 @@
          confirmMsg: submit.msg,
          confirmTitle: submit.title,
          onConfirm: async () => {
            await submit.do();
            submited();
            return;
            return submit.do().then(() => {
              submited();
            });
          }
        });
      }
@@ -116,6 +117,7 @@
        }
      });
    } else {
      formRef.value.clearValidate();
      cancel.do();
    }
  };
@@ -142,5 +144,14 @@
    }
  };
  return { formObj, formRef, edit, active, onSubmit, onCancel, onReset, clear };
  return {
    formObj,
    formRef,
    edit,
    active,
    onSubmit,
    onCancel,
    onReset,
    clear
  };
}
src/composables/messageBox.js
@@ -1,31 +1,45 @@
import { ElMessageBox, ElNotification, ElMessage } from 'element-plus';
import {
  ElMessageBox,
  ElNotification,
  ElMessage
} from 'element-plus';
function useMessageBoxTip({
  confirmMsg,
  confirmTitle = '提交',
  doneMsg = confirmTitle,
  onConfirm,
  onConfirm
}) {
  ElMessageBox.confirm(confirmMsg, `${confirmTitle}确认`, {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
    type: 'warning'
  })
    .then(async () => {
      let msg = `å·²${doneMsg}`
      let msg = `å·²${doneMsg}`;
      if (typeof onConfirm === 'function') {
        const str = await onConfirm();
        if (str && str != '') {
          msg = `å·²${doneMsg}, ${str}`
        }
        onConfirm()
          .then((res) => {
            if (res && res != '') {
              msg = `å·²${doneMsg}, ${res}`;
            }
            ElNotification({
              title: `${confirmTitle}成功`,
              message: msg,
              type: 'success'
            });
          })
          .catch((err) => {
            let errStr = `${confirmTitle}取消`;
            if (err != 'cancel') {
              errStr = `${confirmTitle}失败, ${err}`;
            }
            ElMessage({
              message: errStr,
              type: 'warning'
            });
          });
      }
      ElNotification({
        title: `${confirmTitle}成功`,
        message: msg,
        type: 'success',
        // offset: 170,
        position: 'bottom-left',
      });
    })
    .catch((err) => {
      let errStr = `${confirmTitle}取消`;
@@ -34,7 +48,7 @@
      }
      ElMessage({
        message: errStr,
        type: 'warning',
        type: 'warning'
      });
    });
}
@@ -43,7 +57,7 @@
  ElMessageBox.confirm(confirmMsg, confirmTitle, {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
    type: 'warning'
  })
    .then(async () => {
      if (typeof onConfirm === 'function') {
src/main.js
@@ -4,6 +4,7 @@
import { createPinia } from 'pinia';
import App from './App.vue';
import { createMap } from './components/map/baseMap';
import { tf, nf } from './utils/textFormat';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import 'element-plus/theme-chalk/src/overlay.scss';
@@ -20,6 +21,8 @@
// é«˜å¾·åœ°å›¾åˆå§‹åŒ–
createMap('container');
// æŒ‚载时间格式化函数至全局
app.config.globalProperties.$tf = tf;
app.config.globalProperties.$nf = nf;
app.use(createPinia()).mount('#app');
src/model/clueQuestion.js
@@ -1,4 +1,4 @@
import { imgUrl } from '@/api/clue/index';
import { imgUrl } from '@/api/index';
function getClueQuestion(data) {
  data.cqFilePath = data.cqFilePath.split(';').map((val) => {
src/model/gridRecord.js
@@ -6,9 +6,29 @@
 * @returns
 */
function getGridRecord(data) {
  const path = util.listToLngLat(data.gSide);
  data.gSide = path;
  return data;
  const _sides = data.giSide.split(';').map((value) => {
    return value.split(',');
  });
  const _data = {
    id: data.giUid,
    schemeId: data.gsId,
    name: data.giName,
    sides: util.listToLngLat(_sides),
    delete: data.giDelete,
    createTime: data.giCreateTime
  };
  return _data;
}
function parseToGridInfo(data) {
  return {
    giUid: data.id,
    gsId: data.schemeId,
    giName: data.name,
    giSide: data.overlays.getPath().join(';'),
    giDelete: data.delete,
    giCreateTime: data.createTime
  };
}
function getGridRecordList(dataList) {
@@ -17,4 +37,4 @@
  });
}
export { getGridRecord, getGridRecordList };
export { getGridRecord, getGridRecordList, parseToGridInfo };
src/stores/grid.js
@@ -2,21 +2,27 @@
import { defineStore } from 'pinia';
import mapGrid from '@/components/map/mapGrid';
import baseMapUtil from '@/components/map/baseMapUtil';
import gridInfoApi from '@/api/grid/gridInfoApi';
import { parseToGridInfo } from '@/model/gridRecord';
export const useGridStore = defineStore('grid', () => {
  // å½“前加载的网格数据集合
  const gridList = ref([]);
  // å½“前是否有选中方案
  const hasScheme = ref(false);
  // å½“前选中操作的网格信息
  const selectedGrid = ref({});
  const selectedGrid = ref();
  // å½“前选中操作的网格是否处于可编辑状态
  const isEdit = ref(false);
  // è®°å½•当前网格路径信息
  var selectedPath = undefined;
  /**
   * æ£€æŸ¥å½“前是否有选中的网格
   * @returns {Boolean}
   */
  function _checkGridExist() {
    return selectedGrid.value.gId != undefined;
    return selectedGrid.value && selectedGrid.value.id != undefined;
  }
  /**数据增删**************************************************************/
@@ -27,7 +33,16 @@
  function setGrid(index) {
    if (index >= 0 && index < gridList.value.length) {
      selectedGrid.value = gridList.value[index];
      selectedPath = selectedGrid.value.overlays.getPath().join(';');
    }
  }
  /**
   * å–消选中
   */
  function clearSelect() {
    selectedGrid.value = undefined;
    selectedPath = undefined;
  }
  /**
@@ -36,6 +51,7 @@
   */
  function setGridList(list) {
    gridList.value = list;
    hasScheme.value = true;
  }
  /**
@@ -88,7 +104,14 @@
   */
  function saveSelectedGrid() {
    if (!_checkGridExist()) return;
    selectedGrid.value.gSide = selectedGrid.value.overlays.getPath();
    const newPath = selectedGrid.value.overlays.getPath().join(';');
    if (selectedPath != newPath) {
      const _data = parseToGridInfo(selectedGrid.value);
      return gridInfoApi.updateGrid(_data).then(() => {
        selectedGrid.value.sides =
          selectedGrid.value.overlays.getPath();
      });
    }
  }
  /**
@@ -98,7 +121,7 @@
    if (!_checkGridExist()) return;
    baseMapUtil.removeView(selectedGrid.value.overlays);
    selectedGrid.value.overlays = mapGrid.drawPolygon(
      selectedGrid.value.gSide
      selectedGrid.value.sides
    );
  }
@@ -112,7 +135,7 @@
      showGrid(i);
      grids.push(l.overlays);
    });
    baseMapUtil.setFitView(grids, 1);
    baseMapUtil.setFitView(...grids);
  }
  /**
@@ -123,15 +146,17 @@
    if (item.overlays) {
      baseMapUtil.setFitView(item.overlays);
    } else {
      item.overlays = mapGrid.drawPolygon(item.gSide);
      item.overlays = mapGrid.drawPolygon(item.sides);
    }
  }
  return {
    hasScheme,
    gridList,
    selectedGrid,
    isEdit,
    setGrid,
    clearSelect,
    setGridList,
    addGrid,
    deleteGrid,
src/utils/textFormat.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
/**
 * æ–‡æœ¬æ ·å¼æ ¼å¼åŒ–
 */
import moment from 'moment';
function tf(timeStr) {
  return moment(timeStr).format('YYYY-MM-DD HH:mm:ss');
}
function nf(num, digit = 2) {
  let numStr = num + '';
  while (numStr.length < digit) {
    numStr = '0' + numStr;
  }
  return numStr;
}
export { tf, nf };
src/views/HomePage.vue
@@ -1,16 +1,22 @@
<template>
  <BaseMap></BaseMap>
  <div class="overlay-container">
    <CoreHeader @on-change="(e) => (menuIndex = e)"></CoreHeader>
    <!-- <router-view> -->
    <!-- <GridLayout></GridLayout> -->
    <ClueLayout></ClueLayout>
    <ClueLayout v-show="menuIndex == 0"></ClueLayout>
    <GridLayout v-show="menuIndex == 1"></GridLayout>
    <!-- </router-view> -->
  </div>
</template>
<script setup>
import { ref } from 'vue';
import GridLayout from '@/views/overlay-grid/GridLayout.vue';
import ClueLayout from '@/views/overlay-clue/ClueLayout.vue';
// é¤å•索引
const menuIndex = ref(0);
</script>
<style scoped>
@@ -23,7 +29,7 @@
  height: 100vh;
  top: 0;
  left: 0;
  padding: 4px;
  /* padding: 4px; */
  pointer-events: none;
}
</style>
src/views/overlay-clue/ClueLayout.vue
@@ -1,119 +1,32 @@
<template>
  <el-row class="container" justify="space-between">
    <el-col :span="6" class="grid-content bg-content">
      <div class="fy-h1">线索清单</div>
      <div class="search-wrap">
        <span>时间</span>
        <el-date-picker
          v-model="updateTime"
          type="datetime"
          placeholder="选择日期和时间"
        />
        <el-button type="primary" @click="getClues">查询</el-button>
        <el-button type="primary" @click="fetchRemoteClue"
          >拉取线索</el-button
        >
      </div>
      <ClueList
        :dataList="clueList"
        @itemSelected="selectClue"
      ></ClueList>
  <el-row class="fy-overlay-container" justify="space-between">
    <el-col :span="6">
      <ClueManage @itemSelected="selectClue"></ClueManage>
    </el-col>
    <el-col :span="6" class="grid-content bg-content-1">
      <div class="fy-h1">线索反馈</div>
      <el-scrollbar height="80vh" class="bg-fill">
        <ClueReport :clueData="selectedClue"></ClueReport>
      </el-scrollbar>
    <el-col :span="6">
      <ClueReport
        v-model:show="show"
        :clueData="selectedClue"
      ></ClueReport>
    </el-col>
  </el-row>
</template>
<script setup>
import ClueList from './components/ClueList.vue';
import ClueReport from './components/ClueReport.vue';
import ClueManage from './list/ClueManage.vue';
import ClueReport from './report/ClueReport.vue';
import { ref } from 'vue';
import clueApi from '@/api/clue/clueApi';
import { onMapMounted } from '@/components/map/baseMap';
import moment from 'moment';
import { ref, watch } from 'vue';
// ä¸‹å‘时间(每次查询大于此时间的数据)
const updateTime = ref(new Date());
// çº¿ç´¢æ¸…单
const clueList = ref([]);
const selectedClue = ref();
/**
 * æŸ¥è¯¢å·²ä¸‹å‘的线索清单
 */
const getClues = function () {
  const now = moment(updateTime.value);
  const sTime = now.format('YYYY-MM-DD HH:mm:ss');
  const eTime = now.add(1, 'month').format('YYYY-MM-DD HH:mm:ss');
  onMapMounted(() => {
    clueApi.getClue({ sTime, eTime }).then((res) => {
      clueList.value = res;
    });
  });
};
function fetchRemoteClue() {
  const time = moment(updateTime.value).format('YYYY-MM-DD HH:mm:ss');
  onMapMounted(() => {
    clueApi.fetchRemoteClue(time).then((res) => {
      clueList.value = res;
    });
  });
}
const show = ref(false);
/**
 * é€‰æ‹©çº¿ç´¢äº‹ä»¶
 */
const selectClue = function (clue) {
  show.value = true;
  selectedClue.value = clue;
};
</script>
<style scoped>
.title {
  font-size: var(--el-font-size-large);
}
.container {
  pointer-events: none;
}
.grid-content {
  /* min-width: 180px; */
  border-radius: var(--el-border-radius-round);
  display: flex;
  flex-direction: column;
  gap: 16px;
  /* padding: 8px 8px; */
  pointer-events: auto;
  box-shadow: var(--el-box-shadow-dark);
}
.bg-content {
  height: 90vh;
  background: white;
  min-width: calc(var(--screen-min-width) / 6);
}
.bg-content-1 {
  height: 90vh;
  background: white;
}
.search-wrap {
  padding: 0 8px;
  display: flex;
  align-items: center;
  gap: 4px;
}
.bg-fill {
  /* background: var(--el-fill-color-extra-light); */
  padding: 0 8px;
}
</style>
<style scoped></style>
src/views/overlay-clue/components/ClueList.vue
ÎļþÒÑɾ³ý
src/views/overlay-clue/components/ClueReport.vue
ÎļþÒÑɾ³ý
src/views/overlay-clue/list/ClueManage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
<template>
  <div class="fy-card">
    <div class="fy-h1">线索清单</div>
    <div class="fy-flex-row">
      <span>时间</span>
      <el-date-picker
        v-model="updateTime"
        type="datetime"
        placeholder="选择日期和时间"
      />
      <el-button type="primary" @click="getClues">查询</el-button>
      <el-button type="primary" @click="fetchRemoteClue" plain
        >拉取线索</el-button
      >
    </div>
    <el-scrollbar height="70vh" class="p-h-1">
      <ClueList
        :dataList="clueList"
        @itemSelected="selectClue"
      ></ClueList>
    </el-scrollbar>
  </div>
</template>
<script setup>
import ClueList from './components/ClueList.vue';
import clueApi from '@/api/clue/clueApi';
import { onMapMounted } from '@/components/map/baseMap';
import moment from 'moment';
import { ref, onMounted } from 'vue';
const emits = defineEmits('itemSelected');
// ä¸‹å‘时间(每次查询大于此时间的数据)
const updateTime = ref();
// çº¿ç´¢æ¸…单
const clueList = ref([]);
/**
 * æŸ¥è¯¢å·²ä¸‹å‘的线索清单
 */
const getClues = function () {
  let sTime;
  let eTime;
  if (updateTime.value) {
    const now = moment(updateTime.value);
    sTime = now.format('YYYY-MM-DD HH:mm:ss');
    eTime = now.add(1, 'month').format('YYYY-MM-DD HH:mm:ss');
  }
  onMapMounted(() => {
    clueApi.getClue({ sTime, eTime }).then((res) => {
      clueList.value = res;
    });
  });
};
function fetchRemoteClue() {
  const time = moment(updateTime.value).format('YYYY-MM-DD HH:mm:ss');
  onMapMounted(() => {
    clueApi.fetchRemoteClue(time).then((res) => {
      clueList.value = res;
    });
  });
}
/**
 * é€‰æ‹©çº¿ç´¢äº‹ä»¶
 */
const selectClue = function (clue) {
  emits('itemSelected', clue);
};
onMounted(() => {
  getClues();
});
</script>
<style scoped>
</style>
src/views/overlay-clue/list/components/ClueList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,138 @@
<template>
  <ul class="list-container">
    <template v-for="(item, index) in dataList" :key="index">
      <li
        :class="
          'list-item ' + (item.selected ? 'list-item__selected' : '')
        "
        @click="selectItem(item)"
        v-if="!item.delete"
      >
        <div class="clue-item">
          <div class="flex gap-1">
            <div class="clue-num">{{ $nf(item.cid) }}</div>
            <el-text class="fy-h1" truncated>{{ item.cclueName }}</el-text>
          </div>
          <div class="flex gap-1">
            <div class="">
              <el-text type="info" size="small">结论:</el-text>
              <el-text size="small">{{ item.conclusionCount + '/1' }}</el-text>
            </div>
            <div class="">
              <el-text type="info" size="small">问题:</el-text>
              <el-text size="small">{{ item.questionCount }}</el-text>
            </div>
            <el-text type="info" size="small">{{item.cuploaded ? '已推送' : '未推送'}}</el-text>
          </div>
          <el-row justify="space-between">
            <span class="flex gap-1">
              <el-tag v-if="item.csiteType" size="default" type="info">{{
                item.csiteType
              }}</el-tag>
              <el-tag v-if="item.cfactor" size="default" effect="" type="danger">{{
                item.cfactor
              }}</el-tag>
            </span>
            <el-text size="small">下发时间:{{
              $tf(item.creleaseTime)
            }}</el-text>
          </el-row>
        </div>
      </li>
    </template>
  </ul>
</template>
<script>
import baseMapUtil from '@/components/map/baseMapUtil';
var _marker;
export default {
  props: {
    dataList: Array
  },
  emits: ['itemSelected'],
  data() {
    return {};
  },
  watch: {},
  methods: {
    // åˆ—表选择
    selectItem(item) {
      this.clearSelect();
      item.selected = true;
      // const lnglat = baseMapUtil.wgs84togcj02(
      //   item.clongitude,
      //   item.clatitude
      // );
      baseMapUtil
        .gpsConvert([item.clongitude, item.clatitude])
        .then((lnglat) => {
          baseMapUtil.removeView(_marker);
          _marker = baseMapUtil.addMarker(lnglat);
        });
      this.$emit('itemSelected', item);
    },
    clearSelect() {
      this.dataList.forEach((e) => {
        e.selected = false;
      });
    }
  }
};
</script>
<style scoped>
.list-container {
  padding: initial;
  /* border: var(--el-border); */
  font-size: var(--el-font-size-base);
}
.list-item {
  padding: 4px;
  list-style-type: none;
  border: var(--el-border);
  border-radius: var(--el-border-radius-base);
  box-shadow: var(--el-box-shadow-lighter);
  margin-bottom: 6px;
  cursor: pointer;
}
.list-item:hover {
  background-color: var(--el-color-primary-light-9);
}
.list-item__selected {
  background-color: var(--el-color-primary-light-9);
}
.v-enter-from,
.v-leave-to {
  opacity: 0;
  transform: translateX(8px);
}
.v-enter-active,
.v-leave-active {
  transition: all 0.3s ease-out;
}
.clue-item {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.clue-num {
  font-size: 16px;
  font-weight: 700;
  font-style: italic;
  color: var(--el-color-primary);
}
.clue-tag {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}
</style>
src/views/overlay-clue/report/ClueReport.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
<template>
  <!-- æ¸…单详情 -->
  <CloseButton v-show="show" @close="closeEdit">
    <el-button
      class="push-btn"
      :type="clueData.cuploaded ? 'success' : 'danger'"
      @click="pushCheck"
      :disabled="clueData.cuploaded"
      ><div class="flex-col">
        <template v-if="clueData.cuploaded">
          <el-icon><Check /></el-icon>
          <div>å·²</div>
          <div>推</div>
          <div>送</div>
        </template>
        <template v-else>
          <el-icon><Upload /></el-icon>
          <div>推</div>
          <div>送</div>
          <div>反</div>
          <div>馈</div>
        </template>
      </div></el-button
    >
    <div class="fy-card">
      <div class="fy-h1">线索反馈</div>
      <el-scrollbar height="80vh" class="p-h-1">
        <ClueReportClue :clue="clueData"></ClueReportClue>
        <ClueReportConclusion
          :clueId="clueData.cid"
        ></ClueReportConclusion>
        <ClueReportQuestion
          :clueId="clueData.cid"
        ></ClueReportQuestion>
      </el-scrollbar>
    </div>
  </CloseButton>
</template>
<script>
import ClueReportClue from './components/ClueReportClue.vue';
import ClueReportConclusion from './components/ClueReportConclusion.vue';
import ClueReportQuestion from './components/ClueReportQuestion.vue';
import { useMessageBoxTip } from '@/composables/messageBox';
import clueApi from "@/api/clue/clueApi";
export default {
  components: {
    ClueReportClue,
    ClueReportConclusion,
    ClueReportQuestion
  },
  props: {
    clueData: {
      type: Object,
      default: () => {
        return {};
      }
    },
    show: Boolean
  },
  emits: ['update:show'],
  data() {
    return {};
  },
  methods: {
    closeEdit() {
      this.$emit('update:show', false);
    },
    pushCheck() {
      useMessageBoxTip({
        confirmMsg: '线索推送后无法再修改结论与问题,确认推送?',
        confirmTitle: '线索推送',
        onConfirm: () => {
          return this.pushClue();
        }
      });
    },
    pushClue() {
      return clueApi.pushClue(this.clueData.cid)
    }
  }
};
</script>
<style scoped>
.push-btn {
  position: absolute;
  z-index: 1;
  top: 2rem;
  left: -2.5rem;
  width: 2.5rem;
  height: initial;
  margin: initial;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* background-color: white; */
  /* border-color: white; */
  /* border-top: 1px solid;
  border-left: 1px solid;
  border-bottom: 1px solid; */
  border-top-right-radius: 0px;
  border-bottom-right-radius: 0px;
  /* box-shadow: var(--el-box-shadow-light); */
}
</style>
src/views/overlay-clue/report/components/ClueReportClue.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
<template>
  <!-- æ¸…单详情 -->
  <DescriptionsList title="线索清单详情">
    <template #extra>
      <el-button type="primary" text size="small" @click="openPDF"
        >查看PDF</el-button
      >
    </template>
    <DescriptionsListItem label="线索编号" :content="clue.cid" />
    <DescriptionsListItem
      label="线索名称"
      :content="clue.cclueName"
    />
    <DescriptionsListItem label="创建时间" :content="$tf(clue.ccreateTime)" />
    <DescriptionsListItem label="下发时间" :content="$tf(clue.creleaseTime)" />
    <DescriptionsListItem
      label="报警站点"
      :content="clue.csiteName"
    />
    <DescriptionsListItem
      label="站点类型"
      :content="clue.csiteType"
    />
    <DescriptionsListItem
      label="线索结论"
      :content="clue.cconclusion"
    />
    <!-- <DescriptionsListItem
      label="站点类型选项"
      :content="clue.cairCheckedOptions"
    /> -->
  </DescriptionsList>
</template>
<script>
import clueApi from '@/api/clue/clueApi';
export default {
  props: {
    clue: Object
  },
  methods: {
    openPDF() {
      window.open(
        clueApi.fetchRemoteClueFileUrl(this.clue.cid),
        '_blank'
      );
    }
  },
};
</script>
src/views/overlay-clue/report/components/ClueReportConclusion.vue
ÎļþÃû´Ó src/views/overlay-clue/components/ClueReportConclusion.vue ÐÞ¸Ä
@@ -1,31 +1,31 @@
<template>
  <div class="fy-h2">
    çº¿ç´¢ç»“论
    <el-button
      v-if="conclusion"
      type="warning"
      size="small"
      plain
      icon="Upload"
      @click="pushConclusion"
      :disabled="pushing ? true : conclusion.ccUploaded"
      >{{ pushing ? '推送中' : pushText }}</el-button
    >
  </div>
  <el-descriptions v-if="conclusion" :column="1" size="small" border>
    <el-descriptions-item width="1px" min-width="30px">
      <template #label>
        <div class="descriptions-item">问题类型</div>
      </template>
      {{ conclusion.ccQuestionType }}
    </el-descriptions-item>
    <el-descriptions-item label="线索结论">
      {{ conclusion.ccConclusion }}
    </el-descriptions-item>
    <el-descriptions-item label="详细描述">
      {{ conclusion.ccDetails }}
    </el-descriptions-item>
  </el-descriptions>
  <div class="fy-h2">线索结论</div>
  <DescriptionsList v-if="conclusion">
    <!-- <template #extra>
      <el-button
        v-if="conclusion"
        type="warning"
        size="small"
        plain
        icon="Upload"
        @click="pushConclusion"
        :disabled="pushing ? true : conclusion.ccUploaded"
        >{{ pushing ? '推送中' : pushText }}</el-button
      >
    </template> -->
    <DescriptionsListItem
      label="问题类型"
      :content="conclusion.ccQuestionType"
    />
    <DescriptionsListItem
      label="线索结论"
      :content="conclusion.ccConclusion"
    />
    <DescriptionsListItem
      label="详细描述"
      :content="conclusion.ccDetails"
    />
  </DescriptionsList>
  <div v-else class="fy-dashed-border">
    <el-empty :image-size="50" description="线索结论未上传">
      <el-button type="primary" @click="openDialog"
@@ -187,7 +187,5 @@
});
</script>
<style scoped>
.descriptions-item {
  /* background: aqua; */
}
</style>
src/views/overlay-clue/report/components/ClueReportQuestion.vue
ÎļþÃû´Ó src/views/overlay-clue/components/ClueReportQuestion.vue ÐÞ¸Ä
@@ -1,17 +1,11 @@
<template>
  <div class="fy-h2">线索问题</div>
  <template v-if="questionList.length > 0">
    <el-descriptions
      v-for="(item, index) in questionList"
      :title="'问题 ' + item.cqUid"
      :key="index"
      :column="2"
      size="small"
      border
    >
      <template #extra>
        <el-button-group>
          <el-button
    <template v-for="(item, index) in questionList" :key="index">
      <DescriptionsList :title="item.cqUid">
        <template #extra>
          <el-button-group>
            <!-- <el-button
            type="warning"
            size="small"
            plain
@@ -25,41 +19,30 @@
                ? '推送中'
                : '推送问题'
            }}</el-button
          >
          <el-button
            type="primary"
            size="small"
            @click="checkQuestion(item)"
            >问题详情</el-button
          >
        </el-button-group>
      </template>
      <el-descriptions-item
        width="1px"
        min-width="70px"
        label="问题编号"
      >
        {{ item.cqUid }}
      </el-descriptions-item>
      <el-descriptions-item label="所在街镇">
        {{ item.cqStreet }}
      </el-descriptions-item>
      <el-descriptions-item label="问题描述">
        {{ item.cqDescription }}
      </el-descriptions-item>
      <!-- <el-descriptions-item label="详细地址">
        {{ item.cqAddress }}
      </el-descriptions-item>
      <el-descriptions-item label="经度">
        {{ item.cqLongitude }}
      </el-descriptions-item>
      <el-descriptions-item label="纬度">
        {{ item.cqLatitude }}
      </el-descriptions-item>
      <el-descriptions-item label="创建时间">
        {{ item.cqCreateTime }}
      </el-descriptions-item> -->
    </el-descriptions>
          > -->
            <el-button
              type="primary"
              size="small"
              @click="checkQuestion(item)"
              >问题详情</el-button
            >
          </el-button-group>
        </template>
        <DescriptionsListItem
          label="问题名称"
          :content="item.cqName"
        />
        <DescriptionsListItem
          label="所在街镇"
          :content="item.cqStreet"
        />
        <DescriptionsListItem
          label="问题描述"
          :content="item.cqDescription"
        />
      </DescriptionsList>
      <!-- <el-divider /> -->
    </template>
    <div class="btn-wrap">
      <el-button type="primary" @click="openDialog"
        >添加问题</el-button
@@ -128,7 +111,6 @@
    item.pushing = true;
  });
}
</script>
<style scoped>
.btn-wrap {
src/views/overlay-clue/report/components/QuestionDetail.vue
ÎļþÃû´Ó src/views/overlay-clue/components/QuestionDetail.vue ÐÞ¸Ä
@@ -7,7 +7,7 @@
    destroy-on-close
  >
    <template #header>
      <span> åé¦ˆé—®é¢˜</span>
      <span> æ·»åŠ é—®é¢˜</span>
    </template>
    <el-form
      label-width="90px"
@@ -16,11 +16,17 @@
      :model="formObj"
      ref="formRef"
    >
      <el-form-item label="问题名称" prop="cqName">
        <el-input
          v-model="formObj.cqName"
          placeholder="请输入问题名称"
        ></el-input>
      </el-form-item>
      <el-form-item label="问题描述" prop="cqDescription">
        <el-input
          v-model="formObj.cqDescription"
          type="textarea"
          placeholder="请输入详情"
          placeholder="请输入问题描述"
        ></el-input>
      </el-form-item>
      <el-form-item label="所在街镇" prop="cqStreet">
@@ -34,7 +40,10 @@
        </el-select>
      </el-form-item>
      <el-form-item label="详细地址" prop="cqAddress">
        <el-input v-model="formObj.cqAddress"></el-input>
        <el-input
          v-model="formObj.cqAddress"
          placeholder="请输入地址或者通过“坐标拾取”自动获得"
        ></el-input>
      </el-form-item>
      <el-form-item label="坐标" prop="coordinate">
        <el-input
@@ -57,6 +66,7 @@
          accept="image/png, image/jpeg"
          :limit="3"
          multiple
          :on-preview="handleFilePreview"
          :on-remove="handleFileRemove"
          :on-change="handleFileChange"
        >
@@ -80,6 +90,13 @@
      >
    </template>
  </el-dialog>
  <el-image-viewer
    v-if="previewShow"
    :url-list="urlList"
    :initial-index="initialIndex"
    :infinite="false"
    @close="closePreview"
  ></el-image-viewer>
  <MapSearch
    v-model:show="mapDialogShow"
    @on-submit="selectAddress"
@@ -99,13 +116,30 @@
  question: Object
});
const emit = defineEmits(['update:show', 'onSubmit'], ['onClose']);
const emit = defineEmits(['update:show', 'onSubmit', 'onClose']);
// ä¸ŠæŠ¥å¼¹å‡ºæ¡†
const dialogShow = ref(false);
const mapDialogShow = ref(false);
const uploadRef = ref();
const fileList = ref([]);
const previewShow = ref(false);
const initialIndex = ref(0);
const urlList = computed(() =>
  fileList.value.map((value) => {
    return value.url;
  })
);
function handleFilePreview(file) {
  initialIndex.value = fileList.value.indexOf(file);
  previewShow.value = true;
}
function closePreview() {
  previewShow.value = false;
}
function handleFileRemove(file, fileList) {
  formObj.value.files = fileList;
@@ -127,6 +161,13 @@
const loading = ref(false);
// è¡¨å•检查规则
const rules = reactive({
  cqName: [
    {
      required: true,
      message: '问题名称不能为空',
      trigger: 'blur'
    }
  ],
  cqDescription: [
    {
      required: true,
@@ -182,6 +223,7 @@
  const coor = formObj.value.coordinate.split(',');
  const q = {
    cId: parseInt(props.clueId),
    cqName: formObj.value.cqName,
    cqDescription: formObj.value.cqDescription,
    cqStreet: formObj.value.cqStreet,
    cqAddress: formObj.value.cqAddress,
@@ -238,26 +280,34 @@
      url: f
    });
  });
  return question;
  return { ...question };
}
watch(
  () => props.show,
  () => [props.show, props.question],
  (val) => {
    dialogShow.value = val;
  }
);
watch(
  () => props.question,
  (val) => {
    fileList.value = [];
    if (val) {
      formObj.value = parseFormObj(val);
    } else {
      formObj.value = {};
    dialogShow.value = val[0];
    if (val[0]) {
      fileList.value = [];
      if (val[1]) {
        formObj.value = parseFormObj(val[1]);
      } else {
        formObj.value = {};
      }
    }
  }
);
// watch(
//   () => props.question,
//   (val) => {
//     fileList.value = [];
//     if (val) {
//       formObj.value = parseFormObj(val);
//     } else {
//       formObj.value = {};
//     }
//   }
// );
watch(dialogShow, (val) => {
  emit('update:show', val);
});
src/views/overlay-grid/GridLayout.vue
@@ -1,16 +1,36 @@
<template>
  <el-row class="container">
    <el-col :span="4" class="grid-content bg-content">
      <div class="title">网格化管理</div>
      <OptionGridRecord v-model:value="gridRecord"></OptionGridRecord>
      <ListGridDetail></ListGridDetail>
      <GridCreate
        :is-active="dialogVisible"
        :is-create="true"
      ></GridCreate>
  <el-row class="fy-body fy-overlay-container" gutter="0">
    <el-col :span="6">
      <div class="fy-card fy-main">
        <div class="fy-h1">网格化管理</div>
        <div class="fy-flex-row">
          <span>方案</span>
          <OptionGridRecord
            :refresh="newScheme"
            v-model:value="gridSchemeId"
          ></OptionGridRecord>
          <el-button
            :disabled="!gridSchemeId"
            icon="Search"
            type="primary"
            @click="getSchemeList"
            >查询</el-button
          >
          <SchemeCreate @created="newScheme = !newScheme"></SchemeCreate>
        </div>
        <ListGridDetail></ListGridDetail>
        <GridCreate
          :schemeId="gridSchemeId"
          :is-active="dialogVisible"
        ></GridCreate>
      </div>
    </el-col>
    <el-col v-if="gridStore.selectedGrid" :span="4" class="grid-content bg-content-1">
      <GridEditing></GridEditing>
    <el-col v-if="gridStore.selectedGrid" :span="4" :offset="14">
      <div class="fy-column-reverse">
        <!-- <div class="fy-card"> -->
        <GridEditing></GridEditing>
        <!-- </div> -->
      </div>
    </el-col>
  </el-row>
</template>
@@ -20,60 +40,31 @@
import OptionGridRecord from './components/OptionGridRecord.vue';
import GridCreate from './components/GridCreate.vue';
import GridEditing from './components/GridEditing.vue';
import SchemeCreate from './components/SchemeCreate.vue';
import { ref, watch } from 'vue';
import baseMapUtil from '@/components/map/baseMapUtil';
import { onMapMounted } from '@/components/map/baseMap';
import gridRecordApi from '@/api/gridRecordApi';
import gridInfoApi from '@/api/grid/gridInfoApi';
import { useGridStore } from '@/stores/grid';
const gridStore = useGridStore();
// ç½‘格规划方案id
const gridRecord = ref({ value: '' });
// æ–¹æ¡ˆå…·ä½“网格信息集合
const gridSchemeId = ref();
const getList = function () {
const newScheme = ref(false)
// èŽ·å–æ–¹æ¡ˆè®°å½•
function getSchemeList() {
  onMapMounted(() => {
    gridRecordApi
      .getGridRecordDetail(gridRecord.value)
      .then((res) => {
        baseMapUtil.clearMap();
        gridStore.setGridList(res);
      });
    gridInfoApi.fetchGridList(gridSchemeId.value).then((res) => {
      baseMapUtil.clearMap();
      gridStore.setGridList(res);
    });
  });
};
}
watch(gridRecord, getList);
// watch(gridSchemeId, getList);
</script>
<style scoped>
.container {
  pointer-events: none;
}
.title {
  font-size: var(--el-font-size-large);
}
.grid-content {
  min-width: calc(var(--screen-min-width) / 6);
  /* min-width: 180px; */
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 8px 8px;
  pointer-events: auto;
}
.bg-content {
  height: 90vh;
  background: white;
}
.bg-content-1 {
  background: antiquewhite;
  height: 40vh;
  margin-left: 8px;
}
</style>
<style scoped></style>
src/views/overlay-grid/components/GridCreate.vue
@@ -1,8 +1,19 @@
<template>
  <div>
    <el-button type="primary" @click="createGrid">新建网格</el-button>
    <el-button
      :disabled="!gridStore.hasScheme"
      type="primary"
      @click="createGrid"
      >新建网格</el-button
    >
  </div>
  <el-dialog v-model="dialogVisible" title="Tips" width="30%">
  <el-dialog
    v-model="dialogVisible"
    title="新建网格"
    width="600px"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
  >
    <el-form
      :inline="false"
      :model="formObj"
@@ -39,6 +50,8 @@
import { useGridStore } from '@/stores/grid';
import mapGrid from '@/components/map/mapGrid';
import baseMapUtil from '@/components/map/baseMapUtil';
import gridInfoApi from '@/api/grid/gridInfoApi';
import { getGridRecord } from '@/model/gridRecord';
const gridStore = useGridStore();
@@ -56,15 +69,11 @@
};
const props = defineProps({
  //基本信息
  formInfo: Object,
  //是创建或者更新
  isCreate: Boolean,
  //网格方案id
  schemeId: String,
  //
  isActive: Boolean
});
const emit = defineEmits(['onSubmit', 'onCancel']);
const { formObj, formRef, edit, active, onSubmit, onCancel } =
  useFormConfirm({
@@ -88,60 +97,43 @@
  ]
});
// åˆ›å»º
async function create() {
  // loading.value = true;
  // return sceneApi
  //   .createScene(formObj.value)
  //   .then((res) => {
  //     console.log(res);
  //   })
  //   .finally(() => {
  //     loading.value = false;
  //   });
}
// æ›´æ–°
async function update() {
  // loading.value = true;
  // return sceneApi
  //   .updateScene(formObj.value)
  //   .then((res) => {
  //     console.log(res);
  //   })
  //   .finally(() => {
  //     loading.value = false;
  //   });
}
function submit() {
  const newRecord = {
    gId: '1',
    gType: 0,
    gName: formObj.value.gName,
    gSide: tempOverlays.getPath(),
    overlays: tempOverlays
  const gridInfo = {
    gsId: props.schemeId,
    giName: formObj.value.gName,
    giSide: tempOverlays.getPath().join(';')
  };
  gridStore.addGrid(newRecord);
  emit('onSubmit', newRecord);
  dialogVisible.value = false;
  loading.value = true;
  return gridInfoApi
    .createGrid(gridInfo)
    .then((res) => {
      const newRecord = {
        ...res,
        overlays: tempOverlays
      };
  return props.isCreate ? create() : update();
      gridStore.addGrid(getGridRecord(newRecord));
      dialogVisible.value = false;
    })
    // .catch((err) => {
    //   return err;
    // })
    .finally(() => (loading.value = false));
}
function cancel() {
  emit('onCancel');
  baseMapUtil.removeView(tempOverlays);
  dialogVisible.value = false;
}
watch(
  () => props.formInfo,
  (nValue) => {
    formObj.value = nValue;
  },
  { immediate: false }
);
// watch(
//   () => props.formInfo,
//   (nValue) => {
//     formObj.value = nValue;
//   },
//   { immediate: false }
// );
watch(dialogVisible, (nValue) => {
  active.value = nValue;
src/views/overlay-grid/components/GridEditing.vue
@@ -1,18 +1,53 @@
<template>
  <div>当前操作网格: {{ selectedGrid.gName }}</div>
  <div>网格编号:{{ selectedGrid.gId }}</div>
  <div>网格顶点:</div>
  <div v-for="(item, index) in selectedGrid.gSide" :key="index">
    {{ item }}
  </div>
  <div class="btn-group">
    <el-button @click="startOrCompleteEdit">{{
      isEdit ? '保存编辑' : '开始编辑'
    }}</el-button>
    <el-button :disabled="!isEdit" @click="cancelEdit"
      >取消编辑</el-button
    >
  </div>
  <CloseButton @close="closeEdit">
    <div class="fy-card">
      <div class="fy-h2">正在编辑网格</div>
      <div class="fy-main">
        <el-form
          label-position="left"
          :rules="rules"
          :model="selectedGrid"
          ref="formRef"
        >
          <!-- <el-form-item label="编号" prop="id">
          <div>{{ selectedGrid.id }}</div>
        </el-form-item> -->
          <el-form-item label="名称" prop="name">
            <el-input
              v-model="selectedGrid.name"
              placeholder="请输入网格名称"
              clearable
              disabled
            ></el-input>
          </el-form-item>
          <el-form-item label="顶点" prop="sides">
            <el-scrollbar height="100px" class="fy-main-border">
              <div
                class="list-wrapper"
                v-for="(item, index) in selectedGrid.sides"
                :key="index"
              >
                {{ `${index + 1}. ${item}` }}
              </div>
            </el-scrollbar>
          </el-form-item>
        </el-form>
        <div class="btn-group">
          <el-button
            @click="startOrCompleteEdit"
            :loading="loading"
            >{{ isEdit ? '保存编辑' : '开始编辑' }}</el-button
          >
          <el-button
            type="danger"
            :disabled="!isEdit"
            @click="cancelEdit"
            >取消编辑</el-button
          >
        </div>
      </div>
    </div>
  </CloseButton>
</template>
<script>
@@ -30,7 +65,8 @@
  },
  data() {
    return {
      editing: false
      editing: false,
      loading: false
    };
  },
  computed: {
@@ -41,13 +77,18 @@
      'openGridEdit',
      'closeGridEdit',
      'saveSelectedGrid',
      'quitSelectedGrid'
      'quitSelectedGrid',
      'clearSelect'
    ]),
    // å¼€å§‹æˆ–完成保存编辑网格多边形
    startOrCompleteEdit() {
      if (this.isEdit) {
        this.saveSelectedGrid();
        this.closeGridEdit();
        this.loading = true;
        this.saveSelectedGrid()
          .then(() => {
            this.closeGridEdit();
          })
          .finally(() => (this.loading = false));
      } else {
        this.openGridEdit();
      }
@@ -56,12 +97,27 @@
    cancelEdit() {
      this.quitSelectedGrid();
      this.closeGridEdit();
    },
    // å…³é—­ç¼–辑界面
    closeEdit() {
      this.cancelEdit();
      this.clearSelect();
    }
  }
};
</script>
<style scoped>
.wrapper {
  /* position: relative;
  background: antiquewhite;
  padding-right: 10px; */
}
.btn-group {
  display: flex;
}
</style>
.list-wrapper {
  width: 100%;
  /* background-color: var(--el-bg-color-page); */
}
</style>
src/views/overlay-grid/components/ListGridDetail.vue
@@ -14,8 +14,8 @@
            v-if="!item.delete"
          >
            <div style="display: flex; gap: 8px">
              <div>{{ item.gName }}</div>
              <div>{{ item.gSide.length + '/4' }}</div>
              <div>{{ item.name }}</div>
              <div>{{ item.sides.length + '/4' }}</div>
            </div>
            <Transition>
              <el-button
@@ -83,7 +83,7 @@
.list-container {
  /* background-color: antiquewhite; */
  padding: initial;
  height: 50vh;
  height: 60vh;
  overflow: auto;
  overflow-x: hidden;
  border: var(--el-border);
src/views/overlay-grid/components/OptionGridRecord.vue
@@ -2,13 +2,13 @@
  <el-select
    v-model="selectedOptions"
    placeholder="网格化记录"
    style="max-width: 150px"
    style="width: 150px"
  >
    <el-option
      v-for="s in options"
      :key="s.value"
      :label="s.label"
      :value="s"
      :key="s.gsId"
      :label="s.gsName"
      :value="s.gsId"
    />
  </el-select>
</template>
@@ -16,17 +16,14 @@
<script>
// ç½‘格化方案记录选项
import gridRecordApi from '@/api/gridRecordApi';
import gridSchemeApi from '@/api/grid/gridSchemeApi';
export default {
  props: {
    // æ˜¯å¦åœ¨é¦–选项处添加“全部”选项
    // allOption: {
    //   type: Boolean,
    //   default: true,
    // },
    // åˆ·æ–°ä¸‹æ‹‰åˆ—表
    refresh: Boolean,
    // è¿”回结果
    value: Object,
    value: Number
  },
  data() {
    return {
@@ -35,18 +32,29 @@
    };
  },
  watch: {
    selectedOptions: {
      handler(val) {
        this.$emit('update:value', val);
      },
      deep: true,
    // selectedOptions: {
    //   handler(val) {
    //     this.$emit('update:value', val);
    //   },
    //   deep: true
    // },
    selectedOptions(nVal) {
      this.$emit('update:value', nVal);
    },
    refresh() {
      this.getOptions(true);
    }
  },
  methods: {
    getOptions() {
      gridRecordApi.getGridRecords().then((res) => {
        this.options = res;
        this.selectedOptions = res[0];
    getOptions(newScheme) {
      gridSchemeApi.fetchAllSchemes().then((res) => {
        if (res.length > 0) {
          this.options = res;
          this.selectedOptions = newScheme
            ? res[res.length - 1].gsId
            : res[0].gsId;
          // this.selectedOptions = res[0].gsId;
        }
      });
    }
  },
src/views/overlay-grid/components/SchemeCreate.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
<template>
  <el-button icon="Plus" type="success" plain @click="openDialog"
    >新建方案</el-button
  >
  <el-dialog
    v-model="dialogShow"
    width="600px"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    destroy-on-close
  >
    <template #header>
      <span> æ–°å»ºæ–¹æ¡ˆ</span>
    </template>
    <el-form
      label-width="90px"
      label-position="left"
      :rules="rules"
      :model="formObj"
      ref="formRef"
    >
      <el-form-item label="方案名称" prop="gsName">
        <el-input
          style="width: 400px"
          v-model="formObj.gsName"
          placeholder="请输入方案名称"
        ></el-input>
      </el-form-item>
      <el-form-item label="方案描述" prop="gsDescription">
        <el-input
          v-model="formObj.gsDescription"
          type="textarea"
          placeholder="请输入方案描述"
        ></el-input>
      </el-form-item>
      <el-form-item label="创建人" prop="gsCreatorName">
        <el-input
          style="width: 200px"
          v-model="formObj.gsCreatorName"
          placeholder="请输入创建人"
        ></el-input>
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="onCancel">取消</el-button>
      <el-button
        :disabled="!edit"
        type="primary"
        :loading="loading"
        @click="onSubmit"
        >确定</el-button
      >
    </template>
  </el-dialog>
</template>
<script setup>
import { reactive, ref, watch } from 'vue';
import { useFormConfirm } from '@/composables/formConfirm';
import gridSchemeApi from '@/api/grid/gridSchemeApi';
const emit = defineEmits(['created']);
const rules = reactive({
  gsName: [
    {
      required: true,
      message: '方案名称不能为空',
      trigger: 'blur'
    }
  ]
});
const { formObj, formRef, edit, onSubmit, onCancel } = useFormConfirm(
  {
    submit: {
      do: submit
    },
    cancel: {
      do: cancel
    }
  }
);
const dialogShow = ref(false);
function openDialog() {
  dialogShow.value = true;
}
function submit() {
  return gridSchemeApi.createScheme(formObj.value).then(() => {
    // clear()
    emit('created')
    dialogShow.value = false;
  });
}
function cancel() {
  dialogShow.value = false;
}
</script>
vite.config.js
@@ -26,5 +26,8 @@
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    host: '0.0.0.0'
  }
});