riku
2025-04-25 4a836815f12e8ba717702cc8ed431e1b4f96134c
新增内部线索相关管理逻辑
已修改17个文件
已添加4个文件
926 ■■■■ 文件已修改
src/api/clue/clueApi.js 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueConclusionApi.js 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueInternalApi.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueQuestionApi.js 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueTaskApi.js 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/config.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/core/CoreHeader.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/MapSearch.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/map/baseMapUtil.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/model/clueQuestion.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/HomePage.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/internal-clue/InternalClueEdit.vue 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/internal-clue/InternalClueLayout.vue 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/internal-clue/InternalClueManage.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/ClueReport.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/ClueReportClue.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/ClueReportConclusion.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/ClueReportQuestion.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/report/components/QuestionDetail.vue 196 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/overlay-clue/task/ClueTaskEdit.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/clue/clueApi.js
@@ -7,14 +7,14 @@
   * @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}`);
    return $clue.get(`clue/fetch`, {
      params: {
        sTime,
        eTime,
        pageNum,
        pageSize
      }
    });
    // .then((res) => res.data);
  },
src/api/clue/clueConclusionApi.js
@@ -5,25 +5,33 @@
   * èŽ·å–çº¿ç´¢ç»“è®º
   * @param {string} clueId çº¿ç´¢id
   */
  getConclusion(clueId) {
    return $clue.get(`clue/conclusion/fetch?clueId=${clueId}`).then((res) => res.data);
  getConclusion(clueId, internal) {
    return $clue
      .get(`clue/conclusion/fetch`, {
        params: { clueId, internal }
      })
      .then((res) => res.data);
  },
  /**
   * æäº¤çº¿ç´¢ç»“论
   * @param {object} conclusion çº¿ç´¢
   * @returns
   * @returns
   */
  uploadConclusion(conclusion) {
    return $clue.post(`clue/conclusion/upload`, conclusion).then((res) => res.data);
    return $clue
      .post(`clue/conclusion/upload`, conclusion)
      .then((res) => res.data);
  },
  /**
   * æŽ¨é€çº¿ç´¢ç»“论至第三方
   * @param {Array} conclusionIdList çº¿ç´¢id集合
   * @returns
   * @returns
   */
  pushConclusion(conclusionIdList) {
    return $clue.post(`clue/conclusion/push`, conclusionIdList).then((res) => res.data);
    return $clue
      .post(`clue/conclusion/push`, conclusionIdList)
      .then((res) => res.data);
  }
};
src/api/clue/clueInternalApi.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
import { $clue } from '../index';
export default {
  /**
   * æŸ¥è¯¢çº¿ç´¢æ¸…单
   * @param {object} param0
   * @returns
   */
  getInternalClue({ sTime, eTime, pageNum = 1, pageSize = 30 }) {
    return $clue.get(`clue/internal/fetch`, {
      params: {
        sTime,
        eTime,
        pageNum,
        pageSize
      }
    });
  },
  createInternalClue(clueInternal) {
    return $clue.put('clue/internal/create', clueInternal);
  },
  updateInternalClue(clueInternal) {
    return $clue.post('clue/internal/update', clueInternal);
  },
  deleteInternalClue(clueInternal) {
    return $clue.delete('clue/internal/delete', {
      data: clueInternal
    });
  }
};
src/api/clue/clueQuestionApi.js
@@ -6,9 +6,11 @@
   * èŽ·å–å·²æäº¤çš„çº¿ç´¢é—®é¢˜
   * @param {string} clueId çº¿ç´¢id
   */
  getQuestion(clueId) {
  getQuestion(clueId, internal) {
    return $clue
      .get(`clue/question/fetch?clueId=${clueId}`)
      .get(`clue/question/fetch`, {
        params: { clueId, internal }
      })
      .then((res) => {
        return getClueQuestionList(res.data);
      });
@@ -26,11 +28,34 @@
    files.forEach((e) => {
      formData.append('images', e);
    });
    return $clue.post(`clue/question/upload`, formData).then((res) => res.data);
    return $clue
      .post(`clue/question/upload`, formData)
      .then((res) => res.data);
  },
  /**
   * ä¿®æ”¹çº¿ç´¢é—®é¢˜
   * @param {object} question é—®é¢˜æè¿°
   * @param {*} files é—®é¢˜å›¾ç‰‡
   * @param {Array} deleteImgUrl åˆ é™¤çš„图片相对路径,用;分割
   * @returns
   */
  updateQuestion(question, files, deleteImgUrl) {
    const formData = new FormData();
    formData.append('question', JSON.stringify(question));
    formData.append('deleteImg', deleteImgUrl);
    files.forEach((e) => {
      formData.append('images', e);
    });
    return $clue
      .post(`clue/question/update`, formData)
      .then((res) => res.data);
  },
  deleteQuestion(questionId) {
    return $clue.delete(`clue/question`, { params: { questionId } }).then((res) => res.data);
    return $clue
      .delete(`clue/question`, { params: { questionId } })
      .then((res) => res.data);
  },
  uploadQuestionUrl() {
@@ -43,6 +68,8 @@
   * @returns
   */
  pushQuestion(questionIdList) {
    return $clue.post(`clue/question/push`, questionIdList).then((res) => res.data);
    return $clue
      .post(`clue/question/push`, questionIdList)
      .then((res) => res.data);
  }
};
src/api/clue/clueTaskApi.js
@@ -2,13 +2,21 @@
export default {
  /**
   * åˆ›å»ºå†…部线索任务
   * @param {*} clueTask
   * @returns
   */
  createClueTaskInternal(clueTaskInternal) {
    return $clue.put(`clue/task/create/internal`, clueTaskInternal);
  },
  /**
   * åˆ›å»ºçº¿ç´¢ä»»åŠ¡
   * @param {*} clueTask
   * @returns
   */
  createClueTask(clueTask) {
    return $clue
      .put(`clue/task/create`, clueTask)
    return $clue.put(`clue/task/create`, clueTask);
  },
  /**
@@ -17,8 +25,7 @@
   * @returns
   */
  updateClueTask(clueTask) {
    return $clue
      .post(`clue/task/update`, clueTask)
    return $clue.post(`clue/task/update`, clueTask);
  },
  /**
@@ -27,7 +34,15 @@
   * @returns
   */
  fetchClueTask(clueTask) {
    return $clue
      .post(`clue/task/fetch`, clueTask)
    return $clue.post(`clue/task/fetch`, clueTask);
  },
  /**
   * åˆ é™¤çº¿ç´¢ä»»åŠ¡
   * @param {*} clueTask
   * @returns
   */
  deleteClueTask(clueTask) {
    return $clue.post(`clue/task/delete`, clueTask);
  }
};
src/api/config.js
@@ -11,11 +11,6 @@
        // åœ¨å‘送请求之前, æ·»åŠ è¯·æ±‚å¤´
        // config.headers = addHeaders(config.headers);
        console.log('==>请求开始');
        console.log(`${config.baseURL}${config.url}`);
        if (config.data) {
          console.log('==>请求数据', config.data);
        }
        return config;
      },
      function (error) {
@@ -35,6 +30,11 @@
      function (response) {
        // 2xx èŒƒå›´å†…的状态码都会触发该函数。
        // å¯¹å“åº”数据做点什么
        const config = response.config;
        console.log('==>请求开始', `${config.baseURL}${config.url}`);
        if (config.data) {
          console.log('==>请求数据', config.data);
        }
        console.log(response);
        console.log('==>请求结束');
        if (response.status == 200) {
src/api/index.js
@@ -1,7 +1,7 @@
import axios from 'axios';
import { setInterceptors } from "./config";
const debug = true;
const debug = false;
// let url1 = 'http://47.100.191.150:9031/';
// let url1_file = 'http://47.100.191.150:9031';
src/components/core/CoreHeader.vue
@@ -25,7 +25,8 @@
    return {
      radioOptions: [
        { name: '线索管理', label: 0 },
        { name: '网格管理', label: 1 }
        // { name: '网格管理', label: 1 },
        { name: '内部线索', label: 2 },
      ],
      radio1: 0
    };
src/components/map/MapSearch.vue
@@ -1,9 +1,16 @@
<template>
  <el-dialog v-model="dialogShow" width="70%" destroy-on-close>
  <el-dialog
    class="dialog"
    v-model="dialogShow"
    width="70%"
    destroy-on-close
  >
    <template #header>
      <div> åæ ‡æ‹¾å–</div>
      <div>坐标拾取</div>
    </template>
    <div class="fy-tip-red">左键点击地图选取坐标点,或者根据关键字搜索地点</div>
    <div class="fy-tip-red">
      å·¦é”®ç‚¹å‡»åœ°å›¾é€‰å–坐标点,或者根据关键字搜索地点
    </div>
    <el-row>
      <el-col :span="10">
        <el-form
@@ -45,11 +52,14 @@
          <span>{{ searchResult.address }}</span>
          <div>
            <span>{{
              '高德' + searchResult.lon + ', ' + searchResult.lat
              '高德:' + searchResult.lon + ', ' + searchResult.lat
            }}</span>
            <el-divider direction="vertical" />
            <span>{{
              'GPS' + searchResult.gpsLon + ', ' + searchResult.gpsLat
              'GPS:' +
              searchResult.gpsLon +
              ', ' +
              searchResult.gpsLat
            }}</span>
          </div>
        </div>
@@ -94,7 +104,10 @@
    };
  },
  props: {
    show: Boolean
    // å¯¹è¯æ¡†æ˜¾ç¤ºéšè—
    show: Boolean,
    // é»˜è®¤æœç´¢ç‚¹ç»çº¬åº¦ï¼Œ[lng, lat]
    defaultCoor: Array
  },
  data() {
    return {
@@ -146,29 +159,21 @@
        geocoder = new AMap.Geocoder({
          city: '上海' // city æŒ‡å®šè¿›è¡Œç¼–码查询的城市,支持传入城市名、adcode å’Œ citycode
        });
        if (this.defaultCoor) {
          const [lng, lat] = baseMapUtil.wgs84togcj02(
            this.defaultCoor[0],
            this.defaultCoor[1]
          );
          const lnglat = new AMap.LngLat(lng, lat);
          this.setMarker(lnglat);
          this.getAddress(lnglat);
          this.map.setFitView();
        }
        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;
            }
          });
          this.setMarker(ev.lnglat);
          this.getAddress(ev.lnglat);
        });
      });
      // inited = true;
@@ -199,6 +204,29 @@
        }
      });
    },
    getAddress(lnglat) {
      geocoder.getAddress(lnglat, (status, result) => {
        if (status === 'complete' && result.info === 'OK') {
          this.searchResult.address =
            result.regeocode.formattedAddress;
          this.searchResult.lon = lnglat.getLng();
          this.searchResult.lat = lnglat.getLat();
          const [gpsLon, gpsLat] = baseMapUtil.gcj02towgs84(
            this.searchResult.lon,
            this.searchResult.lat
          );
          this.searchResult.gpsLon = gpsLon;
          this.searchResult.gpsLat = gpsLat;
        }
      });
    },
    setMarker(lnglat) {
      this.map.clearMap();
      const marker = new AMap.Marker({
        position: lnglat
      });
      this.map.add(marker);
    },
    submit() {
      this.$emit('onSubmit', this.searchResult);
      this.dialogShow = false;
@@ -218,4 +246,8 @@
  border-radius: var(--el-border-radius-round);
  box-shadow: var(--el-box-shadow);
}
.dialog {
  pointer-events: auto;
}
</style>
src/components/map/baseMapUtil.js
@@ -129,8 +129,10 @@
  /**
   * é«˜å¾·åœ°å›¾åæ ‡è½¬GPS坐标算法
   */
  gcj02towgs84(lng, lat) {
  gcj02towgs84(_lng, _lat) {
    // lat = +latlng = +lng
    const lng = parseFloat(_lng)
    const lat = parseFloat(_lat)
    if (out_of_china(lng, lat)) {
      return [lng, lat];
    } else {
@@ -158,8 +160,10 @@
   * @param lat
   * @returns {*[]}
   */
  wgs84togcj02(lng, lat) {
  wgs84togcj02(_lng, _lat) {
    // lat = +latlng = +lng
    const lng = parseFloat(_lng)
    const lat = parseFloat(_lat)
    if (out_of_china(lng, lat)) {
      return [lng, lat];
    } else {
src/model/clueQuestion.js
@@ -1,7 +1,7 @@
import { $clue } from '@/api/index';
function getClueQuestion(data) {
  data.cqFilePath = data.cqFilePath.split(';').map((val) => {
  data.files = data.cqFilePath.split(';').map((val) => {
    return $clue.imgUrl + val;
  });
  return data;
src/views/HomePage.vue
@@ -5,6 +5,7 @@
    <!-- <router-view> -->
    <ClueLayout v-show="menuIndex == 0"></ClueLayout>
    <GridLayout v-show="menuIndex == 1"></GridLayout>
    <InternalClueLayout v-show="menuIndex == 2"></InternalClueLayout>
    <!-- </router-view> -->
  </div>
</template>
@@ -14,6 +15,7 @@
import GridLayout from '@/views/overlay-grid/GridLayout.vue';
import ClueLayout from '@/views/overlay-clue/ClueLayout.vue';
import InternalClueLayout from '@/views/internal-clue/InternalClueLayout.vue';
// é¤å•索引
const menuIndex = ref(0);
src/views/internal-clue/InternalClueEdit.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,208 @@
<template>
  <el-dialog
    style="pointer-events: auto"
    :model-value="modelValue"
    @update:modelValue="handleDialogChange"
    width="50%"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    destroy-on-close
  >
    <template #header>
      <span> {{ create ? '发布内部线索' : '更新内部线索' }}</span>
    </template>
    <el-form
      label-width="120px"
      label-position="right"
      :rules="rules"
      :model="formObj"
      ref="formRef"
    >
      <el-form-item label="线索名称" prop="cclueName">
        <el-input
          v-model="formObj.cclueName"
          placeholder="请输入线索名称"
          class="w-200"
        ></el-input>
      </el-form-item>
      <el-form-item label="线索描述" prop="cconclusion">
        <el-input
          v-model="formObj.cconclusion"
          type="textarea"
          placeholder="请输入线索描述"
        ></el-input>
      </el-form-item>
      <el-form-item label="详细地址" prop="caddress">
        <el-input
          v-model="formObj.caddress"
          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>
    <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 { ref, reactive, watch, onMounted } from 'vue';
import { useFormConfirm } from '@/composables/formConfirm';
import clueInternalApi from '@/api/clue/clueInternalApi';
import MapSearch from '@/components/map/MapSearch.vue';
const props = defineProps({
  modelValue: Boolean,
  clueData: Object,
  create: {
    type: Boolean,
    default: true
  },
  // è‡ªå®šä¹‰åˆ›å»ºæ–¹æ³•
  onCreate: Function,
  // è‡ªå®šä¹‰æ›´æ–°æ–¹æ³•
  onUpdate: Function
});
const emits = defineEmits(['update:modelValue', 'onSubmit']);
function handleDialogChange(value) {
  emits('update:modelValue', value);
}
const { formObj, formRef, edit, onSubmit, onCancel, clear } =
  useFormConfirm({
    submit: {
      do: submit
    },
    cancel: {
      do: cancel
    }
  });
const loading = ref(false);
// è¡¨å•检查规则
const rules = reactive({
  cclueName: [
    {
      required: true,
      message: '线索名称不能为空',
      trigger: 'blur'
    }
  ],
  caddress: [
    {
      required: true,
      message: '线索地址不能为空',
      trigger: 'blur'
    }
  ],
  coordinate: [
    {
      required: true,
      message: '线索定位不能为空',
      trigger: 'blur'
    }
  ],
});
// å¯¹è¯æ¡†ç¡®è®¤æ“ä½œ
function submit() {
  const param = getParams();
  return props.create
    ? props.onCreate
      ? props.onCreate(param)
      : createClue(param)
    : props.onUpdate
    ? props.onUpdate(param)
    : updateClue(param);
}
// å¯¹è¯æ¡†å–消操作
function cancel() {
  emits('update:modelValue', false);
}
// æ–°å»ºå†…部线索
function createClue(params) {
  clueInternalApi
    .createInternalClue(params)
    .then(() => {
      emits('update:modelValue', false);
      emits('onSubmit');
    })
    .finally(() => {
      loading.value = false;
    });
}
// æ›´æ–°å†…部线索
function updateClue(params) {
  clueInternalApi
    .updateInternalClue(params)
    .then(() => {
      emits('update:modelValue', false);
      emits('onSubmit');
    })
    .finally(() => {
      loading.value = false;
    });
}
// èŽ·å–çº¿ç´¢å¯¹è±¡å‚æ•°
function getParams() {
  const coor = formObj.value.coordinate.split(',');
  return {
    cid: formObj.value.cid,
    cclueName: formObj.value.cclueName,
    cconclusion: formObj.value.cconclusion,
    caddress: formObj.value.caddress,
    clongitude: parseFloat(coor[0]),
    clatitude: parseFloat(coor[1])
  };
}
// åˆå§‹åŒ–线索表单对象
watch(
  () => [props.modelValue, props.clueData],
  (nV, oV) => {
    const [m, d] = nV;
    if (m) {
      formObj.value = {};
      if (d) {
        formObj.value = d;
        formObj.value.coordinate = d.cLongitude + ',' + d.cLatitude;
      }
    }
  }
);
/*********************************************************** */
const mapDialogShow = ref(false);
function openMapDialog() {
  mapDialogShow.value = true;
}
function selectAddress(result) {
  formObj.value.caddress = result.address;
  formObj.value.coordinate = result.gpsLon + ',' + result.gpsLat;
}
</script>
src/views/internal-clue/InternalClueLayout.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
<template>
  <el-row class="fy-overlay-container" justify="space-between">
    <el-col :span="6">
      <InternalClueManage
        @itemSelected="selectClue"
      ></InternalClueManage>
    </el-col>
    <el-col :span="6">
      <ClueReport
        v-model:show="show"
        :clueData="selectedClue"
        @pushed="(e) => (selectedClue.cuploaded = e)"
        @onClueTaskChange="handleClueTaskChange"
      ></ClueReport>
    </el-col>
  </el-row>
</template>
<script setup>
import InternalClueManage from '@/views/internal-clue/InternalClueManage.vue';
import ClueReport from '@/views/overlay-clue/report/ClueReport.vue';
import { ref, provide } from 'vue';
// æ³¨å…¥å‚æ•°
// è¡¨æ˜Žå½“前操作的是内部线索
provide('isInternal', true);
const selectedClue = ref();
const show = ref(false);
/**
 * é€‰æ‹©çº¿ç´¢äº‹ä»¶
 */
const selectClue = function (clue) {
  show.value = true;
  selectedClue.value = clue;
};
function handleClueTaskChange() {
  selectedClue.value.taskCount = 1;
}
</script>
<style scoped></style>
src/views/internal-clue/InternalClueManage.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
<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="success" @click="clueDialog = true"
        >新建</el-button
      >
    </div>
    <el-scrollbar height="70vh" class="p-h-1">
      <ClueList :dataList="clueList" @itemSelected="selectClue">
      </ClueList>
    </el-scrollbar>
    <el-row justify="space-between" class="p-8">
      <el-pagination
        size="small"
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :page-sizes="[10, 20, 50, 100]"
        :background="true"
        layout="total, sizes, pager"
        :total="total"
      />
    </el-row>
  </div>
  <InternalClueEdit
    v-model="clueDialog"
    :create="true"
    :onCreate="createInternalClue"
  ></InternalClueEdit>
  <ClueTaskEdit
    v-model="clueTaskDialog"
    :create="true"
    :onCreate="createInternalTask"
  ></ClueTaskEdit>
</template>
<script setup>
import { ref, onMounted, reactive } from 'vue';
import moment from 'moment';
import clueInternalApi from '@/api/clue/clueInternalApi';
import clueTaskApi from '@/api/clue/clueTaskApi';
import { onMapMounted } from '@/components/map/baseMap';
import ClueList from '@/views/overlay-clue/list/components/ClueList.vue';
import ClueTaskEdit from '@/views/overlay-clue/task/ClueTaskEdit.vue';
import InternalClueEdit from '@/views/internal-clue/InternalClueEdit.vue';
const emits = defineEmits('itemSelected');
// ä¸‹å‘时间(每次查询大于此时间的数据)
const updateTime = ref();
// çº¿ç´¢æ¸…单
const clueList = ref([]);
const currentPage = ref(1);
const pageSize = ref(100);
const total = ref(0);
/**
 * æŸ¥è¯¢å·²ä¸‹å‘的线索清单
 */
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(() => {
    clueInternalApi
      .getInternalClue({
        sTime,
        eTime,
        pageNum: currentPage.value,
        pageSize: pageSize.value
      })
      .then((res) => {
        total.value = res.head.totalCount;
        clueList.value = res.data;
      });
  });
};
/**
 * é€‰æ‹©çº¿ç´¢äº‹ä»¶
 */
const selectClue = function (clue) {
  emits('itemSelected', clue);
};
onMounted(() => {
  getClues();
});
/************************************************************** */
const clueData = ref();
const clueDialog = ref(false);
const clueTaskDialog = ref(false);
function createInternalClue(clue) {
  clueData.value = clue;
  clueDialog.value = false;
  clueTaskDialog.value = true;
}
function createInternalTask(clueTask) {
  clueTaskApi
    .createClueTaskInternal({
      clueTask: clueTask,
      clueInternal: clueData.value
    })
    .then((res) => {
      if (res.success) {
        clueTaskDialog.value = false;
        getClues();
      }
    });
}
</script>
<style scoped></style>
src/views/overlay-clue/report/ClueReport.vue
@@ -2,6 +2,7 @@
  <!-- æ¸…单详情 -->
  <CloseButton v-show="show" @close="closeEdit">
    <el-button
      v-if="!isInternal"
      class="push-btn"
      :type="clueData.cuploaded ? 'success' : 'danger'"
      @click="pushCheck"
@@ -76,6 +77,12 @@
import clueTaskApi from '@/api/clue/clueTaskApi';
export default {
  inject: {
    // æ˜¯å¦æ˜¯å†…部线索相关操作
    isInternal: {
      default: false
    }
  },
  components: {
    ClueReportClue,
    ClueReportConclusion,
@@ -127,7 +134,10 @@
    getClueTask() {
      clueTaskApi
        .fetchClueTask({ clueId: this.clueData.cid })
        .fetchClueTask({
          clueId: this.clueData.cid,
          internalTask: this.isInternal
        })
        .then((res) => {
          this.isCreateMode = res.data.length == 0;
          if (res.data.length > 0) {
src/views/overlay-clue/report/components/ClueReportClue.vue
@@ -2,7 +2,12 @@
  <!-- æ¸…单详情 -->
  <DescriptionsList title="线索清单详情">
    <template #extra>
      <el-button type="primary" text size="small" @click="openPDF"
      <el-button
        v-if="!isInternal"
        type="primary"
        text
        size="small"
        @click="openPDF"
        >查看PDF</el-button
      >
    </template>
@@ -11,8 +16,14 @@
      label="线索名称"
      :content="clue.cclueName"
    />
    <DescriptionsListItem label="创建时间" :content="$tf(clue.ccreateTime)" />
    <DescriptionsListItem label="下发时间" :content="$tf(clue.creleaseTime)" />
    <DescriptionsListItem
      label="创建时间"
      :content="$tf(clue.ccreateTime)"
    />
    <DescriptionsListItem
      label="下发时间"
      :content="$tf(clue.creleaseTime)"
    />
    <DescriptionsListItem
      label="报警站点"
      :content="clue.csiteName"
@@ -36,6 +47,12 @@
import clueApi from '@/api/clue/clueApi';
export default {
  inject: {
    // æ˜¯å¦æ˜¯å†…部线索相关操作
    isInternal: {
      default: false
    }
  },
  props: {
    clue: Object
  },
@@ -46,6 +63,6 @@
        '_blank'
      );
    }
  },
  }
};
</script>
src/views/overlay-clue/report/components/ClueReportConclusion.vue
@@ -74,10 +74,13 @@
</template>
<script setup>
import { reactive, ref, watch, computed } from 'vue';
import { reactive, ref, watch, computed, inject } from 'vue';
import { useCloned } from '@vueuse/core';
import { useFormConfirm } from '@/composables/formConfirm';
import clueConclusionApi from '@/api/clue/clueConclusionApi';
// å†³å®šå½“前是否是内部线索相关操作
const isInternal = inject('isInternal', false);
const props = defineProps({
  clueId: Number
@@ -137,6 +140,7 @@
function submit() {
  formObj.value.cid = props.clueId;
  formObj.value.ccInternal = isInternal;
  return uploadConclusion();
}
function cancel() {
@@ -164,11 +168,13 @@
 * èŽ·å–çº¿ç´¢ç»“è®º
 */
function getConclusion() {
  clueConclusionApi.getConclusion(props.clueId).then((res) => {
    conclusion.value = res;
    formObj.value = res == null ? {} : res;
    // formObj.value = useCloned(res, { manual: true });
  });
  clueConclusionApi
    .getConclusion(props.clueId, isInternal)
    .then((res) => {
      conclusion.value = res;
      formObj.value = res == null ? {} : res;
      // formObj.value = useCloned(res, { manual: true });
    });
}
</script>
<style scoped></style>
src/views/overlay-clue/report/components/ClueReportQuestion.vue
@@ -6,10 +6,10 @@
        <template #extra>
          <div>
            <el-button
              v-if="!clueData.cuploaded"
              type="danger"
              size="small"
              icon="Delete"
              :disabled="clueData.cuploaded"
              @click="deleteQuestion(item)"
            ></el-button>
            <el-button
@@ -55,6 +55,7 @@
  </div>
  <QuestionDetail
    :clueData="clueData"
    :uploaded="clueData.cuploaded"
    v-model:show="dialogShow"
    :question="selectedQuestion"
    @on-submit="getQuestion"
@@ -62,13 +63,16 @@
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import { ref, watch, computed, inject } from 'vue';
import clueQuestionApi from '@/api/clue/clueQuestionApi';
import QuestionDetail from './QuestionDetail.vue';
import {
  useMessageBoxTip,
  useMessageBox
} from '@/composables/messageBox';
// å†³å®šå½“前是否是内部线索相关操作
const isInternal = inject('isInternal', false);
const props = defineProps({
  // clueId: Number,
@@ -129,9 +133,11 @@
 * èŽ·å–çº¿ç´¢ç»“è®º
 */
function getQuestion() {
  clueQuestionApi.getQuestion(props.clueData.cid).then((res) => {
    questionList.value = res;
  });
  clueQuestionApi
    .getQuestion(props.clueData.cid, isInternal)
    .then((res) => {
      questionList.value = res;
    });
}
function pushQuestion(item) {
src/views/overlay-clue/report/components/QuestionDetail.vue
@@ -7,7 +7,11 @@
    destroy-on-close
  >
    <template #header>
      <span> æ·»åŠ é—®é¢˜</span>
      <span>
        {{
          uploaded ? '问题详情' : createMode ? '添加问题' : '修改问题'
        }}</span
      >
    </template>
    <el-form
      label-width="90px"
@@ -18,19 +22,25 @@
    >
      <el-form-item label="问题名称" prop="cqName">
        <el-input
          :disabled="uploaded"
          v-model="formObj.cqName"
          placeholder="请输入问题名称"
        ></el-input>
      </el-form-item>
      <el-form-item label="问题描述" prop="cqDescription">
        <el-input
          :disabled="uploaded"
          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-select
          v-model="formObj.cqStreet"
          placeholder="所在街镇"
          :disabled="uploaded"
        >
          <el-option
            v-for="s in streets"
            :key="s.value"
@@ -41,22 +51,29 @@
      </el-form-item>
      <el-form-item label="详细地址" prop="cqAddress">
        <el-input
          :disabled="uploaded"
          v-model="formObj.cqAddress"
          placeholder="请输入地址或者通过“坐标拾取”自动获得"
        ></el-input>
      </el-form-item>
      <el-form-item label="坐标" prop="coordinate">
        <el-input
          :disabled="uploaded"
          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
          :disabled="uploaded"
          plain
          type="primary"
          @click="openMapDialog"
          >坐标拾取</el-button
        >
      </el-form-item>
      <el-form-item label="问题图片" prop="files">
        <el-upload
          :class="uploadableClz"
          ref="uploadRef"
          :file-list="fileList"
          action=""
@@ -64,16 +81,49 @@
          list-type="picture-card"
          name="images"
          accept="image/png, image/jpeg"
          :limit="3"
          :limit="maxImageCount"
          multiple
          :on-preview="handleFilePreview"
          :on-remove="handleFileRemove"
          :on-change="handleFileChange"
        >
          <el-icon><Plus /></el-icon>
          <template #file="{ file }">
            <div>
              <img
                class="el-upload-list__item-thumbnail"
                :src="file.url"
                alt=""
              />
              <span class="el-upload-list__item-actions">
                <span
                  class="el-upload-list__item-preview"
                  @click="handleFilePreview(file)"
                >
                  <el-icon><zoom-in /></el-icon>
                </span>
                <!-- <span
                  v-if="!disabled"
                  class="el-upload-list__item-delete"
                  @click="handleDownload(file)"
                >
                  <el-icon><Download /></el-icon>
                </span> -->
                <span
                  v-if="!uploaded"
                  class="el-upload-list__item-delete"
                  @click="handleFileRemove(file)"
                >
                  <el-icon><Delete /></el-icon>
                </span>
              </span>
            </div>
          </template>
          <template #tip>
            <div class="el-upload__tip">
              è¯·é€‰æ‹©å°äºŽ500kb的jpg/png图片,最多3å¼ 
              {{
                `请选择小于500kb的jpg/png图片,最多${maxImageCount}å¼ `
              }}
            </div>
          </template>
        </el-upload>
@@ -99,47 +149,78 @@
  ></el-image-viewer>
  <MapSearch
    v-model:show="mapDialogShow"
    :defaultCoor="
      formObj.coordinate
        ? formObj.coordinate.split(',')
        : undefined
    "
    @on-submit="selectAddress"
  ></MapSearch>
</template>
<script setup>
import { reactive, ref, watch, computed } from 'vue';
import { reactive, ref, watch, computed, inject } from 'vue';
import { ElMessage } from 'element-plus';
import { useFormConfirm } from '@/composables/formConfirm';
import { streets } from '@/constant/street';
import clueQuestionApi from '@/api/clue/clueQuestionApi';
import { $clue } from '@/api/index';
import MapSearch from '@/components/map/MapSearch.vue';
// å†³å®šå½“前是否是内部线索相关操作
const isInternal = inject('isInternal', false);
const props = defineProps({
  clueId: Number,
  // åº”急线索对象
  clueData: {
    type: Object,
    default: () => {
      return {};
    }
  },
  // å¯¹è¯æ¡†æ˜¾ç¤ºæŽ§åˆ¶
  show: Boolean,
  // çº¿ç´¢é—®é¢˜å¯¹è±¡
  question: Object,
  create: {
  // é—®é¢˜æ˜¯å¦å·²ä¸Šä¼ 
  uploaded: {
    type: Boolean,
    default: true
    default: false
  },
  maxImageCount: {
    type: Number,
    default: 3
  }
});
const emit = defineEmits(['update:show', 'onSubmit', 'onClose']);
// åˆ›å»ºæˆ–是修改模式
const createMode = ref(true);
// ä¸ŠæŠ¥å¼¹å‡ºæ¡†
const dialogShow = ref(false);
const mapDialogShow = ref(false);
const uploadRef = ref();
const fileList = ref([]);
// æ›´æ–°æ¨¡å¼ä¸‹ï¼Œè®°å½•被删除的原有图片
let deletedFileList = [];
// å†³å®šæ˜¯å¦èƒ½ä¸Šä¼ å›¾ç‰‡
const uploadableClz = computed(() => {
  return props.uploaded ||
    (fileList.value && fileList.value.length >= props.maxImageCount)
    ? 'question-not-upload'
    : '';
});
const previewShow = ref(false);
const initialIndex = ref(0);
const urlList = computed(() =>
  fileList.value.map((value) => {
    return value.url;
  })
  fileList.value
    ? fileList.value.map((value) => {
        return value.url;
      })
    : []
);
function handleFilePreview(file) {
@@ -151,12 +232,22 @@
  previewShow.value = false;
}
function handleFileRemove(file, fileList) {
  formObj.value.files = fileList;
function handleFileRemove(file, files) {
  if (!createMode.value) {
    if (file.url.indexOf($clue.imgUrl) != -1) {
      const originUrl = file.url.replace($clue.imgUrl, '');
      deletedFileList.push(originUrl);
    }
  }
  const index = fileList.value.indexOf(file);
  fileList.value.splice(index, 1);
  // fileList.value = fileList;
  edit.value = true;
}
function handleFileChange(file, fileList) {
  formObj.value.files = fileList;
function handleFileChange(file, files) {
  fileList.value = files;
  edit.value = true;
}
const { formObj, formRef, edit, onSubmit, onCancel, clear } =
@@ -230,23 +321,38 @@
});
function submit() {
  if (!fileList.value || fileList.value.length == 0) {
    ElMessage({
      message: '至少上传一张图片',
      type: 'error'
    });
    return;
  }
  const coor = formObj.value.coordinate.split(',');
  const q = {
    cid: parseInt(props.clueData.cid),
    cqName: formObj.value.cqName,
    cqDescription: formObj.value.cqDescription,
    cqStreet: formObj.value.cqStreet,
    cqAddress: formObj.value.cqAddress,
    ...formObj.value,
    // cqId: formObj.value.cqId,
    cId: parseInt(props.clueData.cid),
    // cqName: formObj.value.cqName,
    // cqDescription: formObj.value.cqDescription,
    // cqStreet: formObj.value.cqStreet,
    // cqAddress: formObj.value.cqAddress,
    cqLongitude: parseFloat(coor[0]),
    cqLatitude: parseFloat(coor[1])
    cqLatitude: parseFloat(coor[1]),
    cqInternal: isInternal
    // cqFilePath: formObj.value.cqFilePath
  };
  const files = [];
  if (formObj.value.files) {
    formObj.value.files.forEach((f) => {
      files.push(f.raw);
  if (fileList.value) {
    fileList.value.forEach((f) => {
      if (f.url.indexOf($clue.imgUrl) == -1) {
        files.push(f.raw);
      }
    });
  }
  return props.create ? uploadQuestion(q, files) : updateQuestion(q, );
  return createMode.value
    ? uploadQuestion(q, files)
    : updateQuestion(q, files);
}
function cancel() {
@@ -268,12 +374,11 @@
 */
function uploadQuestion(question, files) {
  loading.value = true;
  return clueQuestionApi
    .uploadQuestion(question, files)
    .then(() => {
      dialogShow.value = false;
      clear();
      // clear();
      uploadRef.value.clearFiles();
      emit('onSubmit');
    })
@@ -282,19 +387,30 @@
    });
}
function updateQuestion(question, newFiles, deleteFiles) {
function updateQuestion(question, newFiles) {
  loading.value = true;
  const deleteImgUrl = deletedFileList.join(';');
  return clueQuestionApi
    .updateQuestion(question, newFiles, deleteImgUrl)
    .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({
  fileList.value = question.files.map((f, index) => {
    return {
      name: `${index}`,
      url: f
    });
    };
  });
  return { ...question };
}
@@ -305,16 +421,28 @@
    dialogShow.value = val[0];
    if (val[0]) {
      fileList.value = [];
      deletedFileList = [];
      if (val[1]) {
        createMode.value = false;
        formObj.value = parseFormObj(val[1]);
      } else {
        createMode.value = true;
        formObj.value = {};
      }
      // edit.value = false
    }
  }
);
watch(dialogShow, (val) => {
  if (!val) {
    clear();
  }
  emit('update:show', val);
});
</script>
<style scoped>
:deep(.question-not-upload .el-upload-list > .el-upload) {
  display: none;
}
</style>
src/views/overlay-clue/task/ClueTaskEdit.vue
@@ -85,17 +85,16 @@
const props = defineProps({
  modelValue: Boolean,
  clueData: {
    type: Object,
    default: () => {
      return {};
    }
  },
  clueData: Object,
  clueTask: Object,
  create: {
    type: Boolean,
    default: true
  }
  },
  // è‡ªå®šä¹‰åˆ›å»ºæ–¹æ³•
  onCreate: Function,
  // è‡ªå®šä¹‰æ›´æ–°æ–¹æ³•
  onUpdate: Function
});
const emits = defineEmits(['update:modelValue', 'onSubmit']);
@@ -116,13 +115,13 @@
const loading = ref(false);
// è¡¨å•检查规则
const rules = reactive({
  clueId: [
    {
      required: true,
      message: '线索编号不能为空',
      trigger: 'blur'
    }
  ],
  // clueId: [
  //   {
  //     required: true,
  //     message: '线索编号不能为空',
  //     trigger: 'blur'
  //   }
  // ],
  taskTime: [
    {
      required: true,
@@ -155,7 +154,13 @@
function submit() {
  const param = getParams();
  return props.create ? createClueTask(param) : updateClueTask(param);
  return props.create
    ? props.onCreate
      ? props.onCreate(param)
      : createClueTask(param)
    : props.onUpdate
    ? props.onUpdate(param)
    : updateClueTask(param);
}
function cancel() {