- 分页功能
比如获取文件 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
}
- 树形数据
/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>
- 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。
- 返回数组的接口,一定要给分页信息或者其他信息留下扩展的空间
比如,有一个接口,返回的是数组,但是没有分页信息,也没有其他信息,这样的接口,不好扩展。
{
"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"
}
}
希望后端同学,能够注意这个问题。
以上例子可能有了还不够理解这种设计的好处,再举一个例子:
四预【预报预警】-> 【工程信息】 -> 【提防工程】,上图的数据不分页,右侧表格分页,这样的设计,不好扩展。
- 后端排序问题
各人写的排序,字段不同
- 请求河流 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) // 执行回调函数
}
}
- 用户打开菜单,组件渲染后请求接口,接口返回后,上图。用户可能在接口还没返回之前,又把菜单关掉了,这时候,请求回来的数据,仍然会在地图上渲染很多数据。
解决方案:组件销毁时(用户关闭菜单),取消正在进行的请求,以 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: '成功',
// });
// });
});
}
}
- 用户在地图上查看数据的时候,希望能随意拖拽某些面板。
拖拽功能在系统里使用还是挺多的,以 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 }
两个情况会导致组件热更新失效:
- 引入路径和组件路径不一致
SomeDemo.vue
// 组件名称和文件名不一致,能找到模块,但是无法热更新
import SomeDemo from './components/Somedemo.vue'
- 文件路径使用中划线命名
sy-Index/components/SomeDemo.vue
import SomeDemo from '@m/sy-Index/components/SomeDemo.vue'
使用
camelCase
风格命名路径,不要使用中划线。 模块引入路径要全局和模块路径一致。