riku
2023-07-20 d22ce1ad1c4656f5c2212bbabb35ba498300aced
线索下发及提交结论和问题模块基本完成
已修改17个文件
已添加8个文件
1489 ■■■■ 文件已修改
README.md 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueApi.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueConclusionApi.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueQuestionApi.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/index.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/base.css 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/border.css 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/main.css 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/text.css 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | 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 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/formConfirm.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/constant/street.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/model/clueQuestion.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/HomePage.vue 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/ClueLayout.vue 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/ClueList.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/ClueReport.vue 280 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/ClueReportConclusion.vue 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/ClueReportQuestion.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/components/QuestionDetail.vue 264 ●●●●● 补丁 | 查看 | 原始文档 | 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
@@ -2,18 +2,21 @@
export default {
  /**
   * æŸ¥è¯¢å·²ä¸‹å‘的线索清单
   * æŸ¥è¯¢çº¿ç´¢æ¸…单
   * @param {object} param0
   * @returns
   */
  getClues(time) {
    return $clue.get(`feedback/queryYxfList?updateTime=${time}`);
  },
  getClue({ sTime, eTime, pageNum = 1, pageSize = 30 }) {
    return $clue.get(
      `clue/fetch?sTime=${sTime}&eTime=${eTime}&pageNum=${pageNum}&pageSize=${pageSize}`
    );
  },
  /**
   * ä»Žç¬¬ä¸‰æ–¹è¿œç¨‹æ‹‰å–线索清单
   * @param {string} updateTime æ›´æ–°æ—¶é—´ï¼ŒèŽ·å–è¯¥æ—¶é—´ä¹‹åŽçš„çº¿ç´¢
   * @returns
   */
  fetchRemoteClue(updateTime) {
    return $clue.get(`clue/fetch/remote?updateTime=${updateTime}`);
  }
src/api/clue/clueConclusionApi.js
@@ -2,16 +2,27 @@
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
@@ -1,18 +1,43 @@
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}`);
    return $clue
      .get(`clue/question/fetch?clueId=${clueId}`)
      .then((res) => {
        return getClueQuestionList(res);
      });
  },
  /**
   * ä¸Šä¼ çº¿ç´¢é—®é¢˜
   * @param {object} question é—®é¢˜æè¿°
   * @param {*} files é—®é¢˜å›¾ç‰‡
   * @returns
   */
  uploadQuestion(question, files) {
    const formData = {};
    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
@@ -2,17 +2,19 @@
import md5 from 'md5';
import { ElMessage } from 'element-plus';
// const ip = 'http://101.230.224.80:8082/';
const ip = 'http://192.168.0.138:8080/';
const url = 'http://47.100.191.150:9030/';
// const url = 'http://192.168.1.9:8080/';
const imgUrl = url + 'images/';
//飞羽监管
const $clue = axios.create({
  baseURL: ip,
  timeout: 10000,
  baseURL: url,
  timeout: 10000
  // headers: addHeaders()
});
// console.log($clue);
function addHeaders(headers) {
function getHeaders() {
  const token = 'e6dc8bb9e1ff0ce973fb92b4af2e4c3f';
  const date = new Date();
@@ -67,15 +69,17 @@
      console.log('==>请求结束');
      if (response.status == 200) {
        if (
          response.data.code != undefined &&
          response.data.code != null
          response.data.success != undefined &&
          response.data.success != null
        ) {
          if (response.data.code == 20000) {
          if (response.data.success == true) {
            return response.data.data;
          } else {
            return Promise.reject(
              response.data.code + ', ' + response.data.message
            );
            ElMessage({
              message: response.data.message,
              type: 'error'
            });
            return Promise.reject(response.data.message);
          }
        } else {
          return response;
@@ -98,4 +102,4 @@
  );
});
export { $clue };
export { $clue, imgUrl };
src/assets/base.css
@@ -63,6 +63,7 @@
  --screen-min-height: 900px;
  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/main.css
@@ -1,8 +1,9 @@
@import './base.css';
@import './border.css';
@import './text.css';
#app {
  margin: 0 auto;
  font-weight: normal;
}
@@ -30,4 +31,4 @@
    grid-template-columns: 1fr 1fr;
    padding: 0 2rem;
  } */
}
}
src/assets/text.css
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
.fy-h1 {
  padding: 8px 8px 8px 8px;
  font-size: var(--el-font-size-large);
}
.fy-h2 {
  padding: 16px 0 8px 0;
  font-size: var(--el-font-size-medium);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.fy-p1 {
}
.fy-p2 {
}
.fy-tip-red {
  font-size: var(--el-font-size-small);
  color: var(--el-color-danger);
}
src/components.d.ts
@@ -9,15 +9,15 @@
  export interface GlobalComponents {
    BaseMap: typeof import('./components/map/BaseMap.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']
    ElDescrrptions: typeof import('element-plus/es')['ElDescrrptions']
    ElDescrrptionsItem: typeof import('element-plus/es')['ElDescrrptionsItem']
    ElDiag: typeof import('element-plus/es')['ElDiag']
    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']
@@ -28,7 +28,8 @@
    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']
    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/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对象
@@ -45,4 +113,56 @@
    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];
    }
  }
};
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,14 @@
    //   manual: true
    // }).cloned.value;
    formRef.value.clearValidate();
    formRef.value.resetFields();
  };
  // æ¸…空表单
  const clear = function () {
    edit.value = false;
    isReset = true;
    formRef.value.resetFields();
    formObj.value = {}
  };
  // æäº¤æˆåŠŸåŽ
@@ -141,5 +142,5 @@
    }
  };
  return { formObj, formRef, edit, active, onSubmit, onCancel, onReset };
  return { formObj, formRef, edit, active, onSubmit, onCancel, onReset, clear };
}
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/model/clueQuestion.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
import { imgUrl } from '@/api/clue/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/views/HomePage.vue
@@ -17,7 +17,9 @@
.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;
src/views/overlay-clue/ClueLayout.vue
@@ -1,7 +1,7 @@
<template>
  <el-row class="container">
  <el-row class="container" justify="space-between">
    <el-col :span="6" class="grid-content bg-content">
      <div class="title">下发线索清单</div>
      <div class="fy-h1">线索清单</div>
      <div class="search-wrap">
        <span>时间</span>
        <el-date-picker
@@ -10,6 +10,9 @@
          placeholder="选择日期和时间"
        />
        <el-button type="primary" @click="getClues">查询</el-button>
        <el-button type="primary" @click="fetchRemoteClue"
          >拉取线索</el-button
        >
      </div>
      <ClueList
        :dataList="clueList"
@@ -17,7 +20,10 @@
      ></ClueList>
    </el-col>
    <el-col :span="6" class="grid-content bg-content-1">
      <ClueReport></ClueReport>
      <div class="fy-h1">线索反馈</div>
      <el-scrollbar height="80vh" class="bg-fill">
        <ClueReport :clueData="selectedClue"></ClueReport>
      </el-scrollbar>
    </el-col>
  </el-row>
</template>
@@ -35,6 +41,7 @@
const updateTime = ref(new Date());
// çº¿ç´¢æ¸…单
const clueList = ref([]);
const selectedClue = ref();
/**
 * æŸ¥è¯¢å·²ä¸‹å‘的线索清单
@@ -42,7 +49,7 @@
const getClues = function () {
  const now = moment(updateTime.value);
  const sTime = now.format('YYYY-MM-DD HH:mm:ss');
  const eTime = now.add(1, 'month');
  const eTime = now.add(1, 'month').format('YYYY-MM-DD HH:mm:ss');
  onMapMounted(() => {
    clueApi.getClue({ sTime, eTime }).then((res) => {
      clueList.value = res;
@@ -50,11 +57,20 @@
  });
};
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) {
  selectedClue.value = clue;
};
</script>
@@ -69,12 +85,13 @@
.grid-content {
  /* min-width: 180px; */
  border-radius: 4px;
  border-radius: var(--el-border-radius-round);
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 8px 8px;
  /* padding: 8px 8px; */
  pointer-events: auto;
  box-shadow: var(--el-box-shadow-dark);
}
.bg-content {
@@ -89,7 +106,14 @@
}
.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>
src/views/overlay-clue/components/ClueList.vue
@@ -8,13 +8,11 @@
            (item.selected ? 'list-item__selected' : '')
          "
          @click="selectItem(item)"
          @mouseover="item.show = true"
          @mouseleave="item.show = false"
          v-if="!item.delete"
        >
          <div style="display: flex; gap: 8px">
            <div>{{ item.id }}</div>
            <div>{{ item.clueName }}</div>
            <div>{{ item.cid }}</div>
            <div>{{ item.cclueName }}</div>
          </div>
        </li>
      </template>
@@ -35,9 +33,15 @@
  methods: {
    // åˆ—表选择
    selectItem(item) {
      this.clearSelect();
      item.selected = true;
      this.$emit('itemSelected', item);
    },
    clearSelect() {}
    clearSelect() {
      this.dataList.forEach((e) => {
        e.selected = false;
      });
    }
  }
};
</script>
@@ -49,6 +53,7 @@
  overflow: auto;
  overflow-x: hidden;
  border: var(--el-border);
  font-size: var(--el-font-size-small);
}
.list-item {
src/views/overlay-clue/components/ClueReport.vue
@@ -1,270 +1,104 @@
<template>
  <!-- æ¸…单详情 -->
  <el-descriptions
    class="margin-top"
    title="With border"
    :column="3"
    :size="size"
    class=""
    title="线索清单详情"
    :column="1"
    size="small"
    border
  >
    <template #extra>
      <el-button type="primary">Operation</el-button>
      <el-button
        type="primary"
        text
        size="small"
        @click="openPDF"
        >查看PDF</el-button
      >
    </template>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          <el-icon :style="iconStyle">
            <user />
          </el-icon>
          Username
        </div>
        <div class="cell-item">线索编号</div>
      </template>
      kooriookami
      {{ clueData.cid }}
    </el-descriptions-item>
    <el-descriptions-item>
    <el-descriptions-item width="65px" min-width="50px">
      <template #label>
        <div class="cell-item">
          <el-icon :style="iconStyle">
            <iphone />
          </el-icon>
          Telephone
        </div>
        <div class="cell-item">线索名称</div>
      </template>
      18100000000
      {{ clueData.cclueName }}
    </el-descriptions-item>
    <el-descriptions-item>
    <!-- <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          <el-icon :style="iconStyle">
            <location />
          </el-icon>
          Place
        </div>
        <div class="cell-item">创建时间</div>
      </template>
      Suzhou
    </el-descriptions-item>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          <el-icon :style="iconStyle">
            <tickets />
          </el-icon>
          Remarks
        </div>
      </template>
      <el-tag size="small">School</el-tag>
    </el-descriptions-item>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          <el-icon :style="iconStyle">
            <office-building />
          </el-icon>
          Address
        </div>
      </template>
      No.1188, Wuzhong Avenue, Wuzhong District, Suzhou, Jiangsu Province
    </el-descriptions-item>
  </el-descriptions>
  <!-- æ¸…单详情 -->
<el-descriptions
class=""
    title="线索清单详情"
    :column="1"
    :size="size"
    border
    >
    <!-- <el-descriptions-item class="inventory-descriptions-item"  v-for="item in 10" :key="item" >
      <template #label>
          <span>{{ item }}</span>
      </template>
      {{ item }}
      {{ clueData.ccreateTime }}
    </el-descriptions-item> -->
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          ä¸»é”®
        </div>
        <div class="cell-item">下发时间</div>
      </template>
      {{ clueData.id }}
      {{ clueData.creleaseTime }}
    </el-descriptions-item>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          çº¿ç´¢åç§°
        </div>
        <div class="cell-item">报警站点</div>
      </template>
      {{ clueData.clueName }}
      {{ clueData.csiteName }}
    </el-descriptions-item>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          çº¿ç´¢ç»“论
        </div>
        <div class="cell-item">站点类型</div>
      </template>
      {{ clueData.conclusion }}
      {{ clueData.csiteType }}
    </el-descriptions-item>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          åˆ›å»ºæ—¶é—´
        </div>
        <div class="cell-item">线索结论</div>
      </template>
      {{ clueData.createTime }}
      {{ clueData.cconclusion }}
    </el-descriptions-item>
    <el-descriptions-item>
    <!-- <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          æŠ¥è­¦ç«™ç‚¹åç§°
        </div>
        <div class="cell-item">站点类型选项</div>
      </template>
      {{ clueData.siteName }}
    </el-descriptions-item>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          ç«™ç‚¹ç±»åž‹
        </div>
      </template>
      {{ clueData.sitetype }}
    </el-descriptions-item>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          ä¸‹å‘æ—¶é—´
        </div>
      </template>
      {{ clueData.xfsj }}
    </el-descriptions-item>
    <el-descriptions-item>
      <template #label>
        <div class="cell-item">
          ç«™ç‚¹ç±»åž‹
        </div>
      </template>
      {{ clueData.airCheckedOptions }}
    </el-descriptions-item>
</el-descriptions>
<el-button type="primary" @click="reportButton">反馈上报</el-button>
<el-dialog  v-if="!this.isClueHave" v-model="isShow" width="50%"  :before-close="handleClose">
  <template #title>
    <span> åé¦ˆä¸ŠæŠ¥</span>
  </template>
   <el-form label-width="120px" label-position="left">
    <el-form-item label="问题类型" >
      <el-radio-group v-model="form.radio">
          <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="线索id">
        <el-input v-model.number="form.id"></el-input>
    </el-form-item>
    <el-form-item label="线索结论">
        <el-input v-model="form.conclusion"></el-input>
    </el-form-item>
    <el-form-item label="详细描述">
      <el-input v-model="form.details" type="textarea" placeholder="请输入详情"></el-input>
    </el-form-item>
  </el-form>
    <template #footer>
      <el-button type="" @click="isShow=false">取消</el-button>
      <el-button @click="report">确定</el-button>
    </template>
</el-dialog>
<el-button type="primary" @click="reportPhoto">图片上传</el-button>
<el-dialog v-model="isShowReportPhoto">
<el-form>
    <el-form-item label="线索id">
      <el-input v-model=""></el-input>
    </el-form-item>
</el-form>
</el-dialog>
      {{ clueData.cairCheckedOptions }}
    </el-descriptions-item> -->
  </el-descriptions>
  <ClueReportConclusion :clueId="clueData.cid"></ClueReportConclusion>
  <ClueReportQuestion :clueId="clueData.cid"></ClueReportQuestion>
</template>
<script>
import axios from 'axios'
import ClueReportConclusion from './ClueReportConclusion.vue';
import ClueReportQuestion from './ClueReportQuestion.vue';
export default {
  // å¸ƒå°”类型 æŽ§åˆ¶åé¦ˆä¸ŠæŠ¥çš„æ ·å¼
  // fasle代表无数据 true代表有数据
  props:{
    isClueHave:{
      type:Boolean,
      default:false
  components: { ClueReportConclusion, ClueReportQuestion },
  props: {
    clueData: {
      type: Object,
      default: () => {
        return {};
      }
    }
  },
  name: 'HomePage',
  data(){
    return{
      // ä¿å­˜ä¸‹å‘线索查询后返回的数组数据
      // æˆ–者是点击的展示结果对应的对象数据
      clueData:{},
      // ä¸ŠæŠ¥å¼¹å‡ºæ¡†
      isShow:false,
  data() {
    return {
      // å›¾ç‰‡ä¸Šä¼ çš„弹出框
      isShowReportPhoto:false,
      // è¡¨å•中
      form:{
        // é—®é¢˜ç±»åž‹ å•选按钮的值
        radio:'',
        // çº¿ç´¢id
        id:0,
        // çº¿ç´¢ç»“论
        conclusion:'',
        // è¯¦ç»†æè¿°
        details:'',
        // ä¿å­˜æäº¤åŽçš„返回结果
        result:''
      },
      photoForm:{
        clueId:0,
        questionDescription:'',
      isShowReportPhoto: false,
      photoForm: {
        clueId: 0,
        questionDescription: ''
      }
    }
    };
  },
  methods:{
    // ç‚¹å‡»ä¸ŠæŠ¥æŒ‰é’®
    reportButton(){
      // å½“该线索id已经有结论了
      if(this.isClueHave === true){
        alert('该线索已经有结论了,不可重复上报')
      }
      else if(this.isClueHave === false){
        this.isShow = !this.isShow
      }
    },
// ç‚¹å‡»åé¦ˆä¸ŠæŠ¥æ—¶
report(){
  //发出请求
  axios.post('').then(response =>{
      // èŽ·å–ç»“æžœ
      this.result = response.data.data
      if(this.result =='上报成功'){
        ElMessage.success(this.result);
        // åªæœ‰ä¸Šä¼ æˆåŠŸæ—¶æ‰å…³é—­å¯¹è¯æ¡†
        this.isShow = false
      }
      else if(this.result =='上报失败'){
        ElMessage.error(this.result);
        // å¦åˆ™æš‚时不关闭对话框
      }
  })
},
reportPhoto(){
}
  methods: {
    openPDF() {
    }
  }
}
};
</script>
<style>
</style>
<style></style>
src/views/overlay-clue/components/ClueReportConclusion.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,193 @@
<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 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>
.descriptions-item {
  /* background: aqua; */
}
</style>
src/views/overlay-clue/components/ClueReportQuestion.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,139 @@
<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
            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>
      <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>
    <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/components/QuestionDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
<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="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"></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-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>
  <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([]);
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({
  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),
    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,
  (val) => {
    dialogShow.value = val;
  }
);
watch(
  () => props.question,
  (val) => {
    fileList.value = [];
    if (val) {
      formObj.value = parseFormObj(val);
    } else {
      formObj.value = {};
    }
  }
);
watch(dialogShow, (val) => {
  emit('update:show', val);
});
</script>