Skip to content

Latest commit

 

History

History
916 lines (799 loc) · 22.5 KB

水旱灾害防御调度指挥系统.md

File metadata and controls

916 lines (799 loc) · 22.5 KB

贵州水旱灾害防御调度指挥系统

可改进的问题

相同功能,接口参数和返回的数据结构不同,导致代码复用困难,不好维护

  1. 分页功能

比如获取文件 station-service/query/findIaZDosumTableByPage 接口:

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "pagination": {
      "total": 3,
      "size": 20,
      "page": 1,
      "count": 1,
      "pages": 1
    },
    "dimensions": [],
    "source": [
      {
        "dnm": "abc.doc",
        "dotype": "2",
        "dct": "xxxxxx",
        "filecd": "001",
        "adcd": "450105002000000",
        "adnm": "江南街道"
      }
    ],
    "indices": null,
    "keymap": null,
    "split": null,
    "warnings": null
  }
}

获取站点 api/station-service/monitorObject/filterObject

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "pageNow": 1,
    "page": null,
    "pageSize": 20,
    "size": null,
    "pages": 226,
    "total": 4519,
    "rows": [
      {
        "mocd": "RR_805MR620",
        "monm": "古迹塘",
        "motype": 1,
        "stcd": "805MR620"
      }
    ],
    "orders": null
  }
}

实时雨情况: station-service/rwdbReport/findManyPptnMaxReportByDate

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "pagination": null,
    "dimensions": [
      "所属市州",
      "所属区县",
      "所属乡镇",
      "站点名称",
      "站点编码",
      "最大1h雨量",
      "最大1h雨量时间",
      "最大3h雨量",
      "最大3h雨量时间",
      "最大6h雨量",
      "最大6h雨量时间",
      "今日雨量",
      "是否超警"
    ],
    "source": [
      [
        "河池市",
        "宜州区",
        "宜州区",
        "永代",
        "805MR550",
        "-",
        "-",
        "-",
        "-",
        "-",
        "-",
        "",
        ""
      ]
    ],
    "indices": null,
    "keymap": null,
    "split": null,
    "warnings": null
  }
}

这些接口都是分页接口,应该统一。

不统一带来的问题,前后端代码复用困难。

前端处理表格序号累加,非常棘手。 比如 单站逐时雨情报表

统一的好处:前后端复用代码,提高效率,方便维护

比如分页表格:

<ESTable url="data-url" :cols="[{label:'序号',type:'index'},{label:'日期',prop:'date'}]" pagination />

建议统一返回格式:

{
  "data": {
    "pagination": {
      "total": 1000,
      "currentPage": 1,
      "pageSize": 20
    },
    "rows": [{ "name": "小明", "age": 24 }],
    "dimensions": [
      { "label": "名字", "prop": "name" },
      { "label": "年纪", "prop": "age" }
    ]
  }
}

参数格式:

{
  "pagination": {
    "currentPage": 1,
    "pageSize": 20
  },
  "name": ""
}

前端根据,dimensions 自定生成表头,哪天接口变化了,前端代码不用改。

dimensions 不要时,表头写死,接口变化了,需要修改前端代码。

{
  "data": {
    "pagination": {
      "total": 1000,
      "currentPage": 1,
      "pageSize": 20
    },
    "rows": [{ "name": "小明", "age": 24 }]
  }
}

还有一种分页参数:

monitor/riskRegion/pageAct

{
  "adcd": "520000000000000",
  "org": "1",
  "keyword": "",
  "pageNum": 2,
  "pageSize": 12
}

另一种

{
  "page": 2,
  "size": 12
}
  1. 树形数据

/api/station-service/monitorObject/countObjectByType

返回结构:

{
  "data": [
    {
      "children": []
    }
  ]
}

/api/station-service/video/findVideoAdcdTree

返回结构:

{
  "data": [
    {
      "subs": []
    }
  ]
}

还有其他分页接口,比如 weatherrisk/getStatisticalDangerDetail ,在此不再举例。

开发依赖和生产依赖不区分

图标用起来比较痛苦

<q-input v-model="text" type="search" placeholder="请输入搜索内容" filled dense class="full-width">
  <template #append>
    <i class="icon iconfont icon-chazhao"></i>
  </template>
  <!-- <template #append>
      <q-icon
        v-if="text !== ''"
        name="close"
        class="cursor-pointer"
        @click="text = ''" />
      <q-icon name="search" />
    </template> -->
</q-input>

其他

  1. jsx 使用 v-model
<ElPagination
  small
  background
  pager-count={props.pagerCount}
  layout={props.paginationLayout}
  total={pageInfo.total}
  currentPage={pageInfo.pageNow}
  onUpdate:currentPage={currentPageChange}
  pageSize={pageInfo.pageSize}
  class="mt-4"
/>

不监听 onUpdate:currentPage 会报警告: [ElPagination] 你使用了一些已被废弃的用法,请参考 el-pagination。

弹窗设置一个合适的默认尺寸

接口问题

  1. 返回数组的接口,一定要给分页信息或者其他信息留下扩展的空间

比如,有一个接口,返回的是数组,但是没有分页信息,也没有其他信息,这样的接口,不好扩展。

{
  "code": 0,
  "msg": "操作成功",
  "data": [
    {
      "adcd": "520000000000000",
      "adnm": "贵州省",
      "org": "1",
    }
  ]
}

哪天需要加上分页信息,就需要改接口,改前端代码,难以扩展。

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "pagination": {
      "total": 1000,
      "currentPage": 1,
      "pageSize": 20
    },
    "rows": [
      {
        "adcd": "520000000000000",
        "adnm": "贵州省",
        "org": "1",
      }
    ]
  }
}

如果一开始就返回这样的数据,哪天需要加上分页信息,接口返回结构变化小,前端代码改动也非常小。

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "rows": [
      {
        "adcd": "520000000000000",
        "adnm": "贵州省",
        "org": "1",
      }
    ]
  }
}

扩展接口:

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "rows": [
      {
        "adcd": "520000000000000",
        "adnm": "贵州省",
        "org": "1",
      }
    ],
    "total": 1000,
    "currentPage": 1,
    "pageSize": 20,
    "other": "otherValue"
  }
}

希望后端同学,能够注意这个问题。

以上例子可能有了还不够理解这种设计的好处,再举一个例子:

四预【预报预警】-> 【工程信息】 -> 【提防工程】,上图的数据不分页,右侧表格分页,这样的设计,不好扩展。

  1. 后端排序问题

各人写的排序,字段不同

难点和亮点

难点

  1. 请求河流 wkt, 接口一次返回,数据量大,前端渲染慢甚至卡死,一次请求一条河流,前端渲染快,但是请求次数多。

解决方案:前端做并发请求控制,分批请求。

/**
 * @description: 异步任务并发控制
 * @example
 *  const _concurrencyControl = new ConcurrencyControl({
 *   maxConcurrencyLimit: 6,// 默认最大并发数为6 因为同一域名下浏览器最大并发数为6
 *    callback: res => {
 *      console.log(res)
 *    },
 *  })
 *  asyncTaskArray.forEach(task => {
 *    _concurrencyControl.push(task())
 *  })
 */
// TODO 优化,增加超时控制 和 取消正在进行的请求
export class ConcurrencyControl {
  maxConcurrencyLimit: number
  taskQueue: any[]
  callback: any
  constructor({ maxConcurrencyLimit = 6, callback = undefined } = {}) {
    this.maxConcurrencyLimit = maxConcurrencyLimit
    this.taskQueue = []
    this.callback = callback
    setTimeout(() => {
      this._runTask()
    })
  }

  push(task: any) {
    this.taskQueue.push(task)
    // this.runTask()
  }

  _runTask() {
    // console.log(this.taskQueue.length)
    if (!this.taskQueue.length) return // 任务队列为空,直接返回
    // const task = this.taskQueue.shift() // 取出当前队头任务
    const needRunTaskCount = Math.min(this.taskQueue.length, this.maxConcurrencyLimit) // 需要执行的任务数量
    const tasks = this.taskQueue.splice(0, needRunTaskCount) // 取出需要执行的任务
    // const taskPromises = tasks.map(task => task()) // 执行任务
    // console.log(tasks.length)
    Promise.all(tasks).then(res => {
      this._finishTask(res)
      this._runTask()
    })
  }

  _finishTask(res) {
    this.callback?.(res) // 执行回调函数
  }
}
  1. 用户打开菜单,组件渲染后请求接口,接口返回后,上图。用户可能在接口还没返回之前,又把菜单关掉了,这时候,请求回来的数据,仍然会在地图上渲染很多数据。

解决方案:组件销毁时(用户关闭菜单),取消正在进行的请求,以 hook 形式封装此功能。

import type { MaybeRef } from '@vueuse/core';
// import type { QDialogOptions } from 'quasar';
import type { Ref, ShallowRef } from 'vue';

import { defHttp } from '/@/utils/http/axios';

// import { store as useStore } from '/@/store';
import { useConfirm } from './useConfirm';

export type AfterHttp<Res, K, P> = (
  result: Res,
  params?: P
) => K extends keyof Res ? Res[K] : Res;
export type CancelHttp<P> = (params: P, oldParams: P) => boolean;
export type BeforeHttp<P> = (params: P, oldParams: P) => P; // | boolean;

export type Options<Res, P, K> = {
  enableWatch?: boolean;
  beforeHttp?: BeforeHttp<P>;
  confirm?: QDialogOptions | boolean;
  shouldCancel?: CancelHttp<P>;
  afterHttp?: AfterHttp<Res, K, P>; // FIXME 提供参数,方便某些情况下,根据参数返回特性的值
  headers?: Recordable;
};
// CHECKME 这和下方有何不同??
// useHttp<Res = unknown, P = Partial<Recordable<unknown>>, K = extends keyof Res | undefined>(
/**
 * 接口请求 hook
 * @example const [contactList, loadingContactList, sendHttp, error] = useHttp('sy-rescue-plan-contact-list', paramRef)
 * @param url 请求 url
 * @param params  请求参数,可以为 Ref  或者普通对象,默认为 {}
 * @param enableSendOnMounted  是否自动请求,可决定发送请求的时机,默认 true,组件创建后立即请求
 * @param options 请求配置对象
 * @param options.shouldCancel 请求前根据参数决定是否发出,返回 true 或者 false,默认 undefined
 * @param options.beforeHttp 请求前转换参数,返回转换后的参数或者false,返回 false ,不发出请求,默认 undefined
 * @param options.afterHttp 请求成功后对数据进行处理,返回处理后的数据,默认 undefined
 * @param options.confirm 请求前是否需要二次确认,boolean 或者 QDialogOptions,默认 false
 * @param options.enableWatch 是否监听参数变化,以便在参数变化后发出请求,默认 true
 * @return [data, loading, sendHttp, error] data:接口返回的数据; loading:是否正在请求; sendHttp:可调用 sendHttp 再次请求。
 */
export function useHttp<
  Res = unknown,
  P = Partial<Recordable<unknown>>,
  K = string,
>(
  url: MaybeRef<string>,
  params: MaybeRef<P> = {} as P,
  enableSendOnMounted = true,
  options: Options<Res, P, K> = { enableWatch: true }
) {
  const { confirmDialog } = useConfirm();
  const _params = reactive(params as unknown as object);
  const _url = ref(url);

  type Data = K extends keyof Res ? Res[K] : Res;
  // NOTE 使用 shallowRef 和 shallowReactive 外部响应不到变化,而 reactive 不方便重置值
  const data = ref<Data>();

  const error = shallowRef<Recordable<string>>(null);
  const loading = ref(false);

  let controller = null; // new AbortController()
  // 不显示地指定为 false,默认开启监听
  const enableWatch = options?.enableWatch !== false;
  enableWatch &&
    watch(
      [_url, _params],
      ([newUrl, newParams], [preUrl, preParams], cleanUp) => {
        cleanUp(() => {
          isSameHttp(preUrl, url, preParams, newParams) && abortLastHttp();
        });
        // @ts-ignore
        newUrl && doHttp(newUrl, unref(newParams), preParams);
      },
      {
        // 默认深度监听,方便处理参数嵌套
        deep: true,
      }
    );

  type SendHttp = (params?: MaybeRef<P>) => Promise<any>;
  let lastUrl = '';
  let lastParams = {};
  const sendHttp: SendHttp = (params = {} as P) => {
    if (isSameHttp(lastUrl, unref(url), lastParams, unref(params))) {
      abortLastHttp();
    }
    return doHttp(unref(url), unref(params));
  };

  onMounted(() => {
    // 组件创建后就立即请求
    enableSendOnMounted && sendHttp(unref(params));
  });
  // 中断请求
  onBeforeUnmount(abortHttp);

  // NOTE 返回数组,解构时可重命名,方便多次调用
  // examples:
  // const [departmentList] = useHttp('sy.reserve-plan-orgs', { category: 4 })

  // 需要在组件内再次调用 sendHttp 请求数据,更新 data loading
  // const [contactList, loadingContactList, sendHttp] = useHttp('sy-rescue-plan-contact-list', paramRef)

  // const [dutyList, loading] = useHttp('sy-rescue-plan-duty', paramRef)

  return [data, loading, sendHttp, error] as [
    Ref<Data>,
    Ref<boolean>,
    SendHttp,
    ShallowRef<Recordable<string>>,
  ];

  async function doHttp(path: string, apiParams: P, oldParams: any = {}) {
    let _apiParams = toRaw(apiParams);
    const _oldParams = toRaw(oldParams);
    if (
      typeof options?.shouldCancel === 'function' &&
      options.shouldCancel(_apiParams, _oldParams)
    ) {
      // 取消请求
      return Promise.resolve(true);
    }
    controller = new AbortController();
    // 默认无需确认
    const isOk = options?.confirm && (await confirmDialog(options.confirm));
    // 用户取消操作
    if (options?.confirm && !isOk) {
      return Promise.reject(Error('userCancel'));
    }

    if (typeof options?.beforeHttp === 'function') {
      _apiParams = options.beforeHttp(_apiParams, _oldParams) as P;
    }
    loading.value = true;
    lastUrl = path;
    lastParams = _apiParams;
    const result = await defHttp
      .post({
        url: path,
        params: _apiParams,
        signal: controller.signal,
        headers: options?.headers ?? {},
      })
      .finally(() => {
        loading.value = false;
      });
    const _result = result as Res;
    const res =
      typeof options?.afterHttp === 'function'
        ? options.afterHttp(_result, _apiParams)
        : result;
    data.value = res;
    return Promise.resolve(res);
  }

  function abortHttp() {
    controller?.abort();
  }
  function abortLastHttp() {
    abortHttp();
    lastUrl = '';
    lastParams = {};
  }

  function isSameHttp(oldUrl, newUrl, oldParams, newParams) {
    const isSameParams =
      JSON.stringify(oldParams) === JSON.stringify(newParams);
    const isSameUrl = newUrl === oldUrl;
    return isSameParams && isSameUrl;
  }
}

useConfirm.ts :

import { ElMessageBox } from 'element-plus';

export function useConfirm() {
  return { confirmDialog };

  function confirmDialog(confirm) {
    let _confirm = {
      message: '确定删除该条数据吗?',
      title: '操作提示',
    };
    if (typeof confirm === 'object') {
      _confirm = {
        ..._confirm,
        ...confirm,
      };
    }

    return new Promise((resolve) => {
      ElMessageBox({
        ..._confirm,
        // message: h('p', null, [
        //   h('span', null, 'Message can be '),
        //   h('i', { style: 'color: teal' }, 'VNode'),
        // ]),
        showCancelButton: true,
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        beforeClose: (action, _1, done) => {
          // if (action === 'confirm') {
          resolve(action === 'confirm');
          done();
          // instance.confirmButtonLoading = true;
          // instance.confirmButtonText = '正在执行...';
          // async().then(() => {
          //   done();
          //   instance.confirmButtonLoading = false;
          // });
          // // setTimeout(() => {
          // //   setTimeout(() => {
          // //     instance.confirmButtonLoading = false;
          // //   }, 300);
          // // }, 3000);
          // } else {
          //   done();
          //   // reject('');
          // }
        },
      });
      // .then((action) => {
      //   ElMessage({
      //     type: 'success',
      //     // message: `action: ${action}`,
      //     message: '成功',
      //   });
      // });
    });
  }
}
  1. 用户在地图上查看数据的时候,希望能随意拖拽某些面板。

拖拽功能在系统里使用还是挺多的,以 hook 形式封装此功能。

import type { MaybeRef } from '@vueuse/core'
import type { VNodeRef } from 'vue'

import { useHover } from './useHover'

export interface DraggableOptions {
  dragTips?: string
  dragZIndex?: number
}

/**
 * 拖拽元素 hook
 * @param enable 是否启用拖拽功能,默认为 true, 可通过 ref 动态控制
 * @param options
 * @param options.dragTips 鼠标移动到可拖拽元素上时的提示
 * @param options.dragZIndex 拖拽时的 z-index,默认为 10,可根据实际情况调整,防止被其他元素遮挡
 */
function useDraggable(
  enable: MaybeRef<boolean> = ref(true),
  options: DraggableOptions = {
    dragTips: '长按鼠标,可拖拽',
    dragZIndex: 121, // 比顶部导航栏的 z-index 大 1
  }
) {
  const title = computed(() => (unref(enable) ? options.dragTips : ''))
  const { setHoverTarget } = useHover({
    in: dragTarget => {
      if (!dragTarget) return
      dragTarget.title = title.value
    },
  })
  const position = reactive({ left: 'auto', top: 'auto' })
  const dragging = ref(false)
  // 拖拽元素
  const dragEle = ref(null)
  /**
   * 设置拖拽元素,必需设置
   * @param ele 拖拽元素,绑定到 ref 的 DOM 或者组件
   * @example <div :ref="setDragEle">我是被拖拽的元素</div>
   */
  const setDragEle: VNodeRef = ele => {
    if (dragEle.value) return
    dragEle.value = ele
  }
  // 拖拽 dragEle.value 时需要定位的元素
  const positionEle = ref(null)
  /**
   * 拖拽时需要定位的元素。如果不设置,则默认是拖拽元素 dragEle.value
   * @param ele 拖拽 dragEle.value 时需要定位的元素,绑定到 ref 的 DOM 或者组件
   * @example <div :ref="setPositionEle">我是拖拽时需要被定位的元素</div>
   */
  const setPositionEle: VNodeRef = ele => {
    if (positionEle.value) return
    positionEle.value = ele
  }
  // 是否绑定事件
  let bindEvent = false
  watchEffect(
    () => {
      if (!unref(enable)) {
        if (bindEvent) {
          dragEle.value.removeEventListener('mousedown', onMousedown)
          onMouseup()
          bindEvent = false
        }
        return
      }
      if (!dragEle.value || bindEvent) return
      if (!positionEle.value) positionEle.value = dragEle.value
      setHoverTarget(dragEle.value)
      position.left = positionEle.value.style.left
      position.top = positionEle.value.style.top
      dragEle.value.addEventListener('mousedown', onMousedown)
      bindEvent = true
    },
    {
      flush: 'post',
    }
  )

  onBeforeUnmount(() => {
    dragEle.value.removeEventListener('mouseup', onMouseup)
  })

  let shiftX = 0
  let shiftY = 0
  let initTransition = ''
  let dragEleInitCursor = ''

  return {
    dragging: readonly(dragging),
    position: readonly(position),
    setDragEle,
    setPositionEle,
  }
  function onMousedown(event) {
    // 鼠标相对于header的初始便宜位置
    shiftX = event.clientX - dragEle.value.getBoundingClientRect().left
    shiftY = event.clientY - dragEle.value.getBoundingClientRect().top
    document.addEventListener('mousemove', onMove)
    dragEle.value.addEventListener('mouseup', onMouseup)

    const { transition, right, bottom } = positionEle.value.style

    if (right !== 'auto') positionEle.value.style.right = 'auto'
    if (bottom !== 'auto') positionEle.value.style.bottom = 'auto'

    moveAt(event)
    // 禁用原生的拖拽事件
    dragEle.value.addEventListener('dragstart', disableDrag)

    dragEleInitCursor = dragEle.value.style.cursor
    dragEle.value.style.cursor = 'move'
    initTransition = transition
    positionEle.value.style.transition = 'all 0 ease'

    document.body.style.userSelect = 'none'
  }
  function onMove(event) {
    moveAt(event)
  }
  function onMouseup() {
    document.removeEventListener('mousemove', onMove)
    dragEle.value.removeEventListener('dragstart', disableDrag)

    positionEle.value.style.transition = initTransition
    dragEle.value.style.cursor = dragEleInitCursor

    document.body.style.userSelect = ''
    dragging.value = false
  }

  function moveAt({ pageX, pageY }) {
    const _left = `${pageX - shiftX}px`
    const _top = `${pageY - shiftY}px`
    position.left = _left
    position.top = _top
    dragging.value = true
    positionEle.value.style.left = _left
    positionEle.value.style.top = _top
    positionEle.value.style.zIndex = options.dragZIndex
  }
  function disableDrag() {
    return false
  }
}

export { useDraggable }

useHover.ts :

import hoverIntent from 'hoverintent'
import { ref } from 'vue'

type InAndOut = {
  in?: (target: HTMLElement) => void
  out?: (target: HTMLElement) => void
}
const options = {
  in: target => undefined,
  out: target => undefined,
}
/**
 * 鼠标移入移出 hook,可设置鼠标停留时间。
 * hover 事件瞬间触发,不能设置停留时间
 * @param inAndOut 移入移除回调
 * @param inAndOut.in 移入回调
 * @param inAndOut.out 移出回调
 * @param updateTarget 是否更新 hover 的目标元素
 * @param opts hoverIntent配置
 * @link https://www.npmjs.com/package/hoverintent
 */
function useHover(inAndOut: InAndOut = options, updateTarget = false, opts = undefined) {
  const isHover = ref(false)
  const target = ref(null)

  watch(
    target,
    (target, oldTarget) => {
      if (target && target !== oldTarget) {
        detectHover(target)
      }
    },
    { flush: 'post' }
  )
  function detectHover(target) {
    const _target = isRef(target) ? target.value : target
    if (!_target) return
    const { in: inTarget, out } = inAndOut
    opts &&
      hoverIntent(
        _target,
        () => {
          inTarget?.(_target)
          isHover.value = true
        },
        () => {
          out?.(_target)
          isHover.value = false
        }
      ).options(opts)

    !opts &&
      hoverIntent(
        _target,
        () => {
          inTarget?.(_target)
          isHover.value = true
        },
        () => {
          out?.(_target)
          isHover.value = false
        }
      )
  }

  return {
    isHover,
    setHoverTarget: (ele, ...rest) => {
      if (!updateTarget && target.value) return
      console.log(ele, rest, target.value, 'zqj log rest')
      let attachData = null
      try {
        attachData = JSON.stringify(rest)
      } catch (error) {
        attachData = ''
      }
      if (attachData) {
        ele.dataset.attachData = attachData
      }
      target.value = ele
    },
  }
}

export { useHover }