已修改25个文件
已删除4个文件
已添加25个文件
3165 ■■■■ 文件已修改
README.md 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueApi.js 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueConclusionApi.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueQuestionApi.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/index.js 100 ●●●●● 补丁 | 查看 | 原始文档 | 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 71 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/border.css 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/layout.css 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/main.css 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/shortcut.css 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/text.css 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 17 ●●●● 补丁 | 查看 | 原始文档 | 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/BaseMap.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/MapSearch.vue 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/baseMap.js 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/baseMapUtil.js 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/mapGrid.js 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/search-option/OptionTime.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/formConfirm.js 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/messageBox.js 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constant/street.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/model/clueQuestion.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/model/gridRecord.js 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/grid.js 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/textFormat.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/HomePage.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/ClueLayout.vue 94 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/ClueList.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/ClueReport.vue 74 ●●●●● 补丁 | 查看 | 原始文档 | 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 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/ClueReportQuestion.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/QuestionDetail.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | 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 | 历史
README.md
@@ -1,4 +1,4 @@
# ff-ai-ep-underway-vue
# ff-ai-ep-underway-vuegit
This template should help get you started developing with Vue 3 in Vite.
index.html
@@ -4,7 +4,7 @@
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
    <title>网格一体化</title>
  </head>
  <body>
    <div id="app"></div>
src/api/clue/clueApi.js
@@ -1,11 +1,43 @@
import { $clue } from './index';
import { $clue } from '../index';
export default {
  /**
   * æŸ¥è¯¢å·²ä¸‹å‘的线索清单
   * æŸ¥è¯¢çº¿ç´¢æ¸…单
   * @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(
      `${url}pageNum=${pageNum}&pageSize=${pageSize}`
    );
  },
  getClues(time) {
    return $clue.get(`feedback/queryYxfList?updateTime=${time}`);
  /**
   * ä»Žç¬¬ä¸‰æ–¹è¿œç¨‹æ‹‰å–线索清单
   * @param {string} updateTime æ›´æ–°æ—¶é—´ï¼ŒèŽ·å–è¯¥æ—¶é—´ä¹‹åŽçš„çº¿ç´¢
   * @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
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
import { $clue } from '../index';
export default {
  /**
   * èŽ·å–çº¿ç´¢ç»“è®º
   * @param {string} clueId çº¿ç´¢id
   */
  getConclusion(clueId) {
    return $clue.get(`clue/conclusion/fetch?clueId=${clueId}`);
  },
  /**
   * æäº¤çº¿ç´¢ç»“论
   * @param {object} conclusion çº¿ç´¢
   * @returns
   */
  uploadConclusion(conclusion) {
    return $clue.post(`clue/conclusion/upload`, conclusion);
  },
  /**
   * æŽ¨é€çº¿ç´¢ç»“论至第三方
   * @param {Array} conclusionIdList çº¿ç´¢id集合
   * @returns
   */
  pushConclusion(conclusionIdList) {
    return $clue.post(`clue/conclusion/push`, conclusionIdList);
  }
};
src/api/clue/clueQuestionApi.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
import { $clue } from '../index';
import { getClueQuestionList } from '@/model/clueQuestion';
export default {
  /**
   * èŽ·å–å·²æäº¤çš„çº¿ç´¢é—®é¢˜
   * @param {string} clueId çº¿ç´¢id
   */
  getQuestion(clueId) {
    return $clue
      .get(`clue/question/fetch?clueId=${clueId}`)
      .then((res) => {
        return getClueQuestionList(res);
      });
  },
  /**
   * ä¸Šä¼ çº¿ç´¢é—®é¢˜
   * @param {object} question é—®é¢˜æè¿°
   * @param {*} files é—®é¢˜å›¾ç‰‡
   * @returns
   */
  uploadQuestion(question, files) {
    const formData = new FormData();
    formData.append('question', JSON.stringify(question));
    files.forEach((e) => {
      formData.append('images', e);
    });
    return $clue.post(`clue/question/upload`, formData);
  },
  uploadQuestionUrl() {
    return `${$clue.defaults.baseURL}clue/question/upload`;
  },
  /**
   * æŽ¨é€çº¿ç´¢é—®é¢˜è‡³ç¬¬ä¸‰æ–¹
   * @param {Array} questionIdList é—®é¢˜id集合
   * @returns
   */
  pushQuestion(questionIdList) {
    return $clue.post(`clue/question/push`, questionIdList);
  }
};
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,11 +6,26 @@
  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; */
  color: var(--color-text);
  background: var(--color-background);
  transition: color 0.5s, background-color 0.5s;
src/assets/border.css
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
.fy-dashed-border {
  border: 1px dashed var(--el-border-color);
  border-radius: var(--el-border-radius-base);
}
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,33 +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
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
.fy-h1 {
  padding: 8px 8px 8px 8px;
  font-size: var(--el-font-size-large);
  font-weight: 600;
}
.fy-h2 {
  padding: 8px 8px;
  font-size: var(--el-font-size-medium);
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 600;
}
.fy-p1 {
}
.fy-p2 {
}
.fy-tip-red {
  font-size: var(--el-font-size-small);
  color: var(--el-color-danger);
}
src/components.d.ts
@@ -8,23 +8,34 @@
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']
    ElSwitch: typeof import('element-plus/es')['ElSwitch']
    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']
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
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/BaseMap.vue
@@ -4,7 +4,6 @@
<script setup>
import { onMounted } from 'vue';
import { map, AMap } from './baseMap';
// window._AMapSecurityConfig = {
//   securityJsCode: '「您申请的安全密钥」'
@@ -17,13 +16,13 @@
  // map.add(marker); //添加到地图
});
</script>
<style>
<style scoped>
#container {
  position: relative;
  width: 100%;
  height: 100vh;
  min-height: 900px;
  min-width: 1440px;
  min-height: var(--screen-min-height);
  min-width: var(--screen-min-width);
  z-index: 0px;
}
</style>
src/components/map/MapSearch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,221 @@
<template>
  <el-dialog v-model="dialogShow" width="70%" destroy-on-close>
    <template #header>
      <div> åæ ‡æ‹¾å–</div>
    </template>
    <div class="fy-tip-red">左键点击地图选取坐标点,或者根据关键字搜索地点</div>
    <el-row>
      <el-col :span="10">
        <el-form
          :inline="true"
          label-width="50px"
          label-position="left"
          :model="formObj"
          ref="formRef"
          destroy-on-close
        >
          <el-form-item label="地址" prop="address">
            <el-input
              v-model="formObj.address"
              placeholder="请输入地址搜索"
            ></el-input>
          </el-form-item>
          <!-- <el-form-item label="经度" prop="lon">
        <el-input
          v-model="formObj.lon"
          style="width: 100px"
        ></el-input>
      </el-form-item>
      <el-form-item label="纬度" prop="lat">
        <el-input
          v-model="formObj.lat"
          style="width: 100px"
        ></el-input>
      </el-form-item> -->
          <el-form-item>
            <el-button type="primary" @click="searchKeyword"
              >搜索</el-button
            >
          </el-form-item>
        </el-form>
      </el-col>
      <el-col :span="12">
        <div v-if="searchResult.address">
          <span>选择地址:</span>
          <span>{{ searchResult.address }}</span>
          <div>
            <span>{{
              searchResult.lon + ', ' + searchResult.lat
            }}</span>
            <el-divider direction="vertical" />
            <span>{{
              searchResult.gpsLon + ', ' + searchResult.gpsLat
            }}</span>
          </div>
        </div>
      </el-col>
      <el-col :span="2">
        <el-button
          :disabled="searchResult.gpsLon == undefined"
          type="success"
          @click="submit"
          >选择</el-button
        >
      </el-col>
    </el-row>
    <div id="mapContainer"></div>
  </el-dialog>
</template>
<script>
import { shallowRef } from 'vue';
import AMapLoader from '@amap/amap-jsapi-loader';
// import { AMap, onMapMounted } from './baseMap';
import baseMapUtil from './baseMapUtil.js';
// var map;
var AMap;
var geocoder;
var inited = false;
// onMapMounted(() => {
//   AMap.plugin('AMap.Geocoder', function () {
//     geocoder = new AMap.Geocoder({
//       city: '上海' // city æŒ‡å®šè¿›è¡Œç¼–码查询的城市,支持传入城市名、adcode å’Œ citycode
//     });
//     // ä½¿ç”¨geocoder做地理/逆地理编码
//   });
// });
export default {
  setup() {
    const map = shallowRef(null);
    return {
      map
    };
  },
  props: {
    show: Boolean
  },
  data() {
    return {
      dialogShow: false,
      formObj: {},
      searchResult: {}
    };
  },
  emits: ['update:show', 'onSubmit'],
  watch: {
    show(val) {
      this.dialogShow = val;
    },
    dialogShow(val) {
      if (val) {
        this.mapInit();
      } else {
        this.formObj = {};
        this.searchResult = {};
        this.map.destroy();
      }
      this.$emit('update:show', val);
    }
  },
  methods: {
    mapInit() {
      // if (!inited) {
      AMapLoader.load({
        key: 'c55f27799afbfa69dc5a3fad90cafe51', // ç”³è¯·å¥½çš„Web端开发者Key,首次调用 load æ—¶å¿…å¡«
        version: '2.0', // æŒ‡å®šè¦åŠ è½½çš„ JS API çš„版本,缺省时默认为 1.4.15
        plugins: ['AMap.Geocoder'] // éœ€è¦ä½¿ç”¨çš„的插件列表,如比例尺'AMap.Scale'等
      }).then((_AMap) => {
        AMap = _AMap;
        this.map = new AMap.Map('mapContainer', {
          rotateEnable: true,
          pitchEnable: true,
          alwaysRender: false,
          showLabel: true,
          showBuildingBlock: true,
          // mapStyle: 'amap://styles/e1e78509de64ddcd2efb4cb34c6fae2a',
          // features: ['bg', 'road'],
          pitch: 0, // åœ°å›¾ä¿¯ä»°è§’度,有效范围 0 åº¦- 83 åº¦
          viewMode: '2D', // åœ°å›¾æ¨¡å¼
          resizeEnable: true,
          center: [121.6039283, 31.25295567],
          zooms: [3, 18],
          zoom: 14
        });
        geocoder = new AMap.Geocoder({
          city: '上海' // city æŒ‡å®šè¿›è¡Œç¼–码查询的城市,支持传入城市名、adcode å’Œ citycode
        });
        this.map.on('click', (ev) => {
          // this.formObj.lon = ev.lnglat.getLng();
          // this.formObj.lat = ev.lnglat.getLat();
          this.map.clearMap();
          const marker = new AMap.Marker({
            position: ev.lnglat
          });
          this.map.add(marker);
          geocoder.getAddress(ev.lnglat, (status, result) => {
            if (status === 'complete' && result.info === 'OK') {
              this.searchResult.address =
                result.regeocode.formattedAddress;
              this.searchResult.lon = ev.lnglat.getLng();
              this.searchResult.lat = ev.lnglat.getLat();
              const [gpsLon, gpsLat] = baseMapUtil.gcj02towgs84(
                this.searchResult.lon,
                this.searchResult.lat
              );
              this.searchResult.gpsLon = gpsLon;
              this.searchResult.gpsLat = gpsLat;
            }
          });
        });
      });
      // inited = true;
      // }
    },
    searchKeyword() {
      const keyWord = this.formObj.address;
      this.map.clearMap();
      geocoder.getLocation(keyWord, (status, result) => {
        if (status === 'complete' && result.info === 'OK') {
          const geocode = result.geocodes[0];
          this.searchResult.address = geocode.formattedAddress;
          this.searchResult.lon = geocode.location.getLng();
          this.searchResult.lat = geocode.location.getLat();
          const [gpsLon, gpsLat] = baseMapUtil.gcj02towgs84(
            this.searchResult.lon,
            this.searchResult.lat
          );
          this.searchResult.gpsLon = gpsLon;
          this.searchResult.gpsLat = gpsLat;
          const marker = new AMap.Marker({
            position: geocode.location
          });
          this.map.add(marker);
          this.map.setFitView(marker);
        }
      });
    },
    submit() {
      this.$emit('onSubmit', this.searchResult);
      this.dialogShow = false;
    }
  }
  // updated() {
  //   this.mapInit();
  // }
};
</script>
<style>
#mapContainer {
  position: relative;
  width: 100%;
  height: 60vh;
  z-index: 0px;
  border-radius: var(--el-border-radius-round);
  box-shadow: var(--el-box-shadow);
}
</style>
src/components/map/baseMap.js
@@ -73,25 +73,25 @@
  satellite.hide();
  map.add([satellite]);
  const rPx = 100;
  const tPx = 110;
  // const rPx = 100;
  // const tPx = 110;
  // æ·»åŠ åœ°å›¾æŽ§åˆ¶å·¥å…·
  map.addControl(
    new AMap.ControlBar({
      position: {
        right: rPx + 'px',
        top: tPx + 'px'
      }
    })
  );
  map.addControl(
    new AMap.ToolBar({
      position: {
        right: rPx + 30 + 'px',
        top: tPx + 90 + 'px'
      }
    })
  );
  // map.addControl(
  //   new AMap.ControlBar({
  //     position: {
  //       right: rPx + 'px',
  //       top: tPx + 'px'
  //     }
  //   })
  // );
  // map.addControl(
  //   new AMap.ToolBar({
  //     position: {
  //       right: rPx + 30 + 'px',
  //       top: tPx + 90 + 'px'
  //     }
  //   })
  // );
  //
  mouseTool = new AMap.MouseTool(map);
src/components/map/baseMapUtil.js
@@ -1,6 +1,74 @@
import { map, AMap } from './baseMap';
import { toRaw } from 'vue';
//定义一些常量
const PI = 3.1415926535897932384626;
const a = 6378245.0; //长半轴
const ee = 0.00669342162296594323; //扁率/*** GCJ02 è½¬æ¢ä¸º WGS84* @param lng* @param lat* @returns {*[]}*/
function transformlat(lng, lat) {
  // lat = +lat lng = +lng
  let ret =
    -100.0 +
    2.0 * lng +
    3.0 * lat +
    0.2 * lat * lat +
    0.1 * lng * lat +
    0.2 * Math.sqrt(Math.abs(lng));
  ret +=
    ((20.0 * Math.sin(6.0 * lng * PI) +
      20.0 * Math.sin(2.0 * lng * PI)) *
      2.0) /
    3.0;
  ret +=
    ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) *
      2.0) /
    3.0;
  ret +=
    ((160.0 * Math.sin((lat / 12.0) * PI) +
      320 * Math.sin((lat * PI) / 30.0)) *
      2.0) /
    3.0;
  return ret;
}
function transformlng(lng, lat) {
  // lat = +latlng = +lng
  let ret =
    300.0 +
    lng +
    2.0 * lat +
    0.1 * lng * lng +
    0.1 * lng * lat +
    0.1 * Math.sqrt(Math.abs(lng));
  ret +=
    ((20.0 * Math.sin(6.0 * lng * PI) +
      20.0 * Math.sin(2.0 * lng * PI)) *
      2.0) /
    3.0;
  ret +=
    ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) *
      2.0) /
    3.0;
  ret +=
    ((150.0 * Math.sin((lng / 12.0) * PI) +
      300.0 * Math.sin((lng / 30.0) * PI)) *
      2.0) /
    3.0;
  return ret;
}
/**
 * åˆ¤æ–­æ˜¯å¦åœ¨å›½å†…,不在国内则不做偏移
 * @param lng
 * @param lat
 * @returns {boolean}
 */
function out_of_china(lng, lat) {
  // çº¬åº¦3.86~53.55,经度73.66~135.05
  return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55);
}
export default {
  /**
   * å°†æ•°ç»„表示的坐标点,转换为AMap.LngLat对象
@@ -12,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);
    }
  },
@@ -34,8 +112,10 @@
   * ç§»é™¤è¦†ç›–物
   */
  removeView(overlays) {
    const _overlays = toRaw(overlays);
    map.remove(_overlays);
    if (overlays) {
      const _overlays = toRaw(overlays);
      map.remove(_overlays);
    }
  },
  /**
@@ -45,4 +125,69 @@
    if (map == undefined) return;
    map.clearMap();
  },
  /**
   * é«˜å¾·åœ°å›¾åæ ‡è½¬GPS坐标算法
   */
  gcj02towgs84(lng, lat) {
    // lat = +latlng = +lng
    if (out_of_china(lng, lat)) {
      return [lng, lat];
    } else {
      let dlat = transformlat(lng - 105.0, lat - 35.0);
      let dlng = transformlng(lng - 105.0, lat - 35.0);
      let radlat = (lat / 180.0) * PI;
      let magic = Math.sin(radlat);
      magic = 1 - ee * magic * magic;
      let sqrtmagic = Math.sqrt(magic);
      dlat =
        (dlat * 180.0) /
        (((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;
      return [mglng, mglat];
    }
  },
  /**
   * WGS84 è½¬æ¢ä¸º GCJ02
   * @param lng
   * @param lat
   * @returns {*[]}
   */
  wgs84togcj02(lng, lat) {
    // lat = +latlng = +lng
    if (out_of_china(lng, lat)) {
      return [lng, lat];
    } else {
      let dlat = transformlat(lng - 105.0, lat - 35.0);
      let dlng = transformlng(lng - 105.0, lat - 35.0);
      let radlat = (lat / 180.0) * PI;
      let magic = Math.sin(radlat);
      magic = 1 - ee * magic * magic;
      let sqrtmagic = Math.sqrt(magic);
      dlat =
        (dlat * 180.0) /
        (((a * (1 - ee)) / (magic * sqrtmagic)) * PI);
      dlng =
        (dlng * 180.0) / ((a / sqrtmagic) * Math.cos(radlat) * PI);
      let mglat = Math.round((lat + dlat) * 1000000) / 1000000;
      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
@@ -1,6 +1,6 @@
// è¡¨å•的确认和取消
import { onActivated, onDeactivated, ref, watch } from 'vue';
import { useCloned } from '@vueuse/core';
// import { useCloned } from '@vueuse/core';
import { useMessageBoxTip, useMessageBox } from './messageBox';
export function useFormConfirm({
@@ -70,13 +70,15 @@
    //   manual: true
    // }).cloned.value;
    formRef.value.clearValidate();
    formRef.value.resetFields();
  };
  // æ¸…空表单
  const clear = function () {
    formRef.value.clearValidate();
    edit.value = false;
    isReset = true;
    formRef.value.resetFields();
    formObj.value = {};
  };
  // æäº¤æˆåŠŸåŽ
@@ -93,9 +95,9 @@
          confirmMsg: submit.msg,
          confirmTitle: submit.title,
          onConfirm: async () => {
            await submit.do();
            submited();
            return;
            return submit.do().then(() => {
              submited();
            });
          }
        });
      }
@@ -115,6 +117,7 @@
        }
      });
    } else {
      formRef.value.clearValidate();
      cancel.do();
    }
  };
@@ -141,5 +144,14 @@
    }
  };
  return { formObj, formRef, edit, active, onSubmit, onCancel, onReset };
  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/constant/street.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
export const streets = [
  { label: '大宁路街道', value: ['310106019', '大宁路街道'] },
  { label: '彭浦新村街道', value: ['310106020', '彭浦新村街道'] },
  { label: '临汾路街道', value: ['310106021', '临汾路街道'] },
  { label: '芷江西路街道', value: ['310106022', '芷江西路街道'] },
  { label: '彭浦镇', value: ['310106101', '彭浦镇'] },
  { label: '江宁路街道', value: ['310106006', '江宁路街道'] },
  { label: '石门二路街道', value: ['310106011', '石门二路街道'] },
  { label: '南京西路街道', value: ['310106012', '南京西路街道'] },
  { label: '静安寺街道', value: ['310106013', '静安寺街道'] },
  { label: '曹家渡街道', value: ['310106014', '曹家渡街道'] },
  { label: '天目西路街道', value: ['310106015', '天目西路街道'] },
  { label: '北站街道', value: ['310106016', '北站街道'] },
  { label: '宝山路街道', value: ['310106017', '宝山路街道'] },
  { label: '共和新路街道', value: ['310106018', '共和新路街道'] }
];
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
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
import { imgUrl } from '@/api/index';
function getClueQuestion(data) {
  data.cqFilePath = data.cqFilePath.split(';').map((val) => {
    return imgUrl + val;
  });
  return data;
}
function getClueQuestionList(dataList) {
  return dataList.map((v) => {
    return getClueQuestion(v);
  });
}
export { getClueQuestion, getClueQuestionList };
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 != 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,27 +1,35 @@
<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>
.overlay-container {
  background: transparent;
  position: absolute;
  width: 100%;
  min-height: var(--screen-min-height);
  min-width: var(--screen-min-width);
  width: 100vw;
  height: 100vh;
  top: 0;
  left: 0;
  padding: 4px;
  /* padding: 4px; */
  pointer-events: none;
}
</style>
src/views/overlay-clue/ClueLayout.vue
@@ -1,90 +1,32 @@
<template>
  <el-row class="container">
    <el-col :span="6" class="grid-content bg-content">
      <div class="title">下发线索清单</div>
      <div class="search-wrap">
        <span>时间</span>
        <el-date-picker
          v-model="updateTime"
          type="datetime"
          placeholder="选择日期和时间"
        />
        <el-button type="primary" @click="getClues">查询</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">
      <ClueReport></ClueReport>
    <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 getClues = function () {
  const time = moment(updateTime.value).format('YYYY-MM-DD HH:mm:ss');
  onMapMounted(() => {
    clueApi.getClues(time).then((res) => {
      clueList.value = res;
    });
  });
};
const selectedClue = ref();
const show = ref(false);
/**
 * é€‰æ‹©çº¿ç´¢äº‹ä»¶
 */
const selectClue = function (clue) {};
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: 4px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 8px 8px;
  pointer-events: auto;
}
.bg-content {
  height: 90vh;
  background: white;
  min-width: calc(var(--screen-min-width) / 6);
}
.bg-content-1 {
}
.search-wrap {
  display: flex;
  gap: 4px;
}
</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
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,191 @@
<template>
  <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"
        >反馈上报</el-button
      >
    </el-empty>
  </div>
  <el-dialog
    v-model="dialogShow"
    width="50%"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
  >
    <template #header>
      <span> åé¦ˆç»“论</span>
    </template>
    <el-form
      label-width="120px"
      label-position="left"
      :rules="rules"
      :model="formObj"
      ref="formRef"
    >
      <el-form-item label="问题类型" prop="ccQuestionType">
        <el-radio-group v-model="formObj.ccQuestionType">
          <el-radio label="有问题">有问题</el-radio>
          <el-radio label="无问题">无问题</el-radio>
          <el-radio label="已解决">已解决</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="线索结论" prop="ccConclusion">
        <el-input v-model="formObj.ccConclusion"></el-input>
      </el-form-item>
      <el-form-item label="详细描述" prop="ccDetails">
        <el-input
          v-model="formObj.ccDetails"
          type="textarea"
          placeholder="请输入详情"
        ></el-input>
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="onCancel">取消</el-button>
      <el-button type="primary" :loading="loading" @click="onSubmit"
        >确定</el-button
      >
    </template>
  </el-dialog>
</template>
<script setup>
import { reactive, ref, watch, computed } from 'vue';
import { useFormConfirm } from '@/composables/formConfirm';
import clueConclusionApi from '@/api/clue/clueConclusionApi';
const props = defineProps({
  clueId: Number
});
watch(
  () => props.clueId,
  () => {
    getConclusion();
  }
);
// æŽ¨é€çŠ¶æ€
const pushing = ref(false);
// çº¿ç´¢ç»“论
const conclusion = ref({});
// ä¸ŠæŠ¥å¼¹å‡ºæ¡†
const dialogShow = ref(false);
const { formObj, formRef, onSubmit, onCancel, clear } =
  useFormConfirm({
    submit: {
      do: submit
    },
    cancel: {
      do: cancel
    }
  });
const loading = ref(false);
// è¡¨å•检查规则
const rules = reactive({
  ccQuestionType: [
    {
      required: true,
      message: '问题类型不能为空',
      trigger: 'change'
    }
  ],
  ccConclusion: [
    {
      required: true,
      message: '线索结论不能为空',
      trigger: 'blur'
    }
  ],
  ccDetails: [
    {
      required: true,
      message: '详细描述不能为空',
      trigger: 'blur'
    }
  ]
});
// æ‰“开上报反馈对话框
function openDialog() {
  dialogShow.value = true;
}
function submit() {
  formObj.value.cid = props.clueId;
  return uploadConclusion();
}
function cancel() {
  dialogShow.value = false;
}
/**
 * ä¸Šä¼ çº¿ç´¢ç»“论
 */
function uploadConclusion() {
  loading.value = true;
  return clueConclusionApi
    .uploadConclusion(formObj.value)
    .then(() => {
      dialogShow.value = false;
      clear();
      getConclusion();
    })
    .finally(() => {
      loading.value = false;
    });
}
/**
 * èŽ·å–çº¿ç´¢ç»“è®º
 */
function getConclusion() {
  clueConclusionApi.getConclusion(props.clueId).then((res) => {
    conclusion.value = res;
  });
}
function pushConclusion() {
  clueConclusionApi
    .pushConclusion([conclusion.value.ccId])
    .then(() => {
      pushing.value = true;
    });
}
const pushText = computed(() => {
  return conclusion.value.ccUploaded ? '已推送' : '推送问题';
});
</script>
<style scoped>
</style>
src/views/overlay-clue/report/components/ClueReportQuestion.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
<template>
  <div class="fy-h2">线索问题</div>
  <template v-if="questionList.length > 0">
    <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
            icon="Upload"
            @click="pushQuestion(item)"
            :disabled="item.pushing ? true : item.cqUploaded"
            >{{
              item.cqUploaded
                ? '已推送'
                : item.pushing
                ? '推送中'
                : '推送问题'
            }}</el-button
          > -->
            <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
      >
    </div>
  </template>
  <div v-else class="fy-dashed-border">
    <el-empty :image-size="50" description="无线索问题">
      <el-button type="primary" @click="openDialog"
        >反馈上报</el-button
      >
    </el-empty>
  </div>
  <QuestionDetail
    :clueId="clueId"
    v-model:show="dialogShow"
    :question="selectedQuestion"
    @on-submit="getQuestion"
  ></QuestionDetail>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import clueQuestionApi from '@/api/clue/clueQuestionApi';
import QuestionDetail from './QuestionDetail.vue';
const props = defineProps({
  clueId: Number
});
// çº¿ç´¢ç»“论
const questionList = ref([]);
// ä¸ŠæŠ¥å¼¹å‡ºæ¡†
const dialogShow = ref(false);
const selectedQuestion = ref();
watch(
  () => props.clueId,
  () => {
    getQuestion();
  }
);
// æ‰“开上报反馈对话框
function openDialog() {
  selectedQuestion.value = undefined;
  dialogShow.value = true;
}
// æŸ¥çœ‹é—®é¢˜è¯¦æƒ…
function checkQuestion(item) {
  selectedQuestion.value = item;
  dialogShow.value = true;
}
/**
 * èŽ·å–çº¿ç´¢ç»“è®º
 */
function getQuestion() {
  clueQuestionApi.getQuestion(props.clueId).then((res) => {
    questionList.value = res;
  });
}
function pushQuestion(item) {
  clueQuestionApi.pushQuestion([item.cqId]).then(() => {
    item.pushing = true;
  });
}
</script>
<style scoped>
.btn-wrap {
  display: flex;
  justify-content: center;
  padding: 16px;
}
</style>
src/views/overlay-clue/report/components/QuestionDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,314 @@
<template>
  <el-dialog
    v-model="dialogShow"
    width="50%"
    :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="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="请输入问题描述"
        ></el-input>
      </el-form-item>
      <el-form-item label="所在街镇" prop="cqStreet">
        <el-select v-model="formObj.cqStreet" placeholder="所在街镇">
          <el-option
            v-for="s in streets"
            :key="s.value"
            :label="s.label"
            :value="s.label"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="详细地址" prop="cqAddress">
        <el-input
          v-model="formObj.cqAddress"
          placeholder="请输入地址或者通过“坐标拾取”自动获得"
        ></el-input>
      </el-form-item>
      <el-form-item label="坐标" prop="coordinate">
        <el-input
          style="width: 300px; margin-right: 8px"
          v-model="formObj.coordinate"
          placeholder="经纬度坐标,格式为121.123452,31.231235"
        ></el-input>
        <el-button plain type="primary" @click="openMapDialog"
          >坐标拾取</el-button
        >
      </el-form-item>
      <el-form-item label="问题图片" prop="files">
        <el-upload
          ref="uploadRef"
          :file-list="fileList"
          action=""
          :auto-upload="false"
          list-type="picture-card"
          name="images"
          accept="image/png, image/jpeg"
          :limit="3"
          multiple
          :on-preview="handleFilePreview"
          :on-remove="handleFileRemove"
          :on-change="handleFileChange"
        >
          <el-icon><Plus /></el-icon>
          <template #tip>
            <div class="el-upload__tip">
              è¯·é€‰æ‹©å°äºŽ500kb的jpg/png图片,最多3å¼ 
            </div>
          </template>
        </el-upload>
      </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>
  <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"
  ></MapSearch>
</template>
<script setup>
import { reactive, ref, watch, computed } from 'vue';
import { useFormConfirm } from '@/composables/formConfirm';
import { streets } from '@/constant/street';
import clueQuestionApi from '@/api/clue/clueQuestionApi';
import MapSearch from '@/components/map/MapSearch.vue';
const props = defineProps({
  clueId: Number,
  show: Boolean,
  question: Object
});
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;
}
function handleFileChange(file, fileList) {
  formObj.value.files = fileList;
}
const { formObj, formRef, edit, onSubmit, onCancel, clear } =
  useFormConfirm({
    submit: {
      do: submit
    },
    cancel: {
      do: cancel
    }
  });
const loading = ref(false);
// è¡¨å•检查规则
const rules = reactive({
  cqName: [
    {
      required: true,
      message: '问题名称不能为空',
      trigger: 'blur'
    }
  ],
  cqDescription: [
    {
      required: true,
      message: '问题描述不能为空',
      trigger: 'blur'
    }
  ],
  cqStreet: [
    {
      required: true,
      message: '所在街镇不能为空',
      trigger: 'change'
    }
  ],
  cqAddress: [
    {
      required: true,
      message: '详细地址不能为空',
      trigger: 'blur'
    }
  ],
  coordinate: [
    {
      required: true,
      message: '坐标不能为空',
      trigger: 'blur'
    }
  ],
  // cqLongitude: [
  //   {
  //     required: true,
  //     message: '经度不能为空',
  //     trigger: 'blur'
  //   }
  // ],
  // cqLatitude: [
  //   {
  //     required: true,
  //     message: '维度不能为空',
  //     trigger: 'blur'
  //   }
  // ],
  files: [
    {
      required: true,
      message: '图片不能为空',
      trigger: 'change'
    }
  ]
});
function submit() {
  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,
    cqLongitude: parseFloat(coor[0]),
    cqLatitude: parseFloat(coor[1])
  };
  const files = [];
  formObj.value.files.forEach((f) => {
    files.push(f.raw);
  });
  return uploadQuestion(q, files);
}
function cancel() {
  // clear();
  dialogShow.value = false;
}
function openMapDialog() {
  mapDialogShow.value = true;
}
function selectAddress(result) {
  formObj.value.cqAddress = result.address;
  formObj.value.coordinate = result.gpsLon + ',' + result.gpsLat;
}
/**
 * ä¸Šä¼ çº¿ç´¢ç»“论
 */
function uploadQuestion(question, files) {
  loading.value = true;
  return clueQuestionApi
    .uploadQuestion(question, files)
    .then(() => {
      dialogShow.value = false;
      clear();
      uploadRef.value.clearFiles();
      emit('onSubmit');
    })
    .finally(() => {
      loading.value = false;
    });
}
function parseFormObj(question) {
  question.coordinate =
    question.cqLongitude + ',' + question.cqLatitude;
  fileList.value = [];
  question.cqFilePath.forEach((f, index) => {
    fileList.value.push({
      name: `${index}`,
      url: f
    });
  });
  return { ...question };
}
watch(
  () => [props.show, props.question],
  (val) => {
    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);
});
</script>
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'
  }
});