J'Blog

开发记录

开发记录

登录

token失效问题

日常项目都是用token作为登录凭证,token都具有有效期,用户长时间停留在当前页面未做任何操作情况下,超过有效期token则会失效,为了避免用户在进行繁杂的操作过程中token突然失效退出登录导致前面的操作都无效的问题,可以有以下解决方案:

  1. 设计用户自行暂存功能
  2. token自动刷新并继续请求当前接口 http请求保存当前请求,监测401状态值,调用刷新token接口并重新发送当前请求
  3. 监测用户鼠标移动,利用防抖定时请求用户信息接口
    // 监听鼠标是否在移动,若在移动则定时请求用户信息接口以防在操作过程中token失效问题
    window.addEventListener('mousemove', throttle(() => {
      if (token) getUserInfo()
    }, 1000 * 60 * 10))

审批流程

Activiti是一个开源的工作流引擎,用于在Java应用程序中集成业务流程管理(BPM)。在Activiti中,审批流程通常涉及以下步骤:

部署流程定义:将BPMN 2.0兼容的流程定义文件部署到Activiti引擎。 启动流程实例:使用流程定义创建一个新的流程实例。 任务列表:查询并获取参与者的任务。 完成任务:处理任务,可能会触发流程中的下一个节点。

动态获取菜单并根据平铺的菜单节点生成树

const permissions = [
    {
    	"menuId": 1,
    	"name": "首页",
    	"permission": "/",
    	"parentId": 0,
    	"path": "/",
    	"sort": 0,
    	"type": "0", // 是否是菜单
    	"keepAlive": null,
    	"privilegeIcon": "home"
    },
    ...
]

function getItem(
  label: React.ReactNode,
  key: React.Key,
  icon?: React.ReactNode,
  children?: MenuItem[],
  type?: 'group',
  permission: boolean = true
): MenuItem {
  return {
    key,
    icon,
    children,
    label,
    type,
    permission
  } as MenuItem
}

const getFormatItem = (item, children) => {
    return getItem(item?.name, item?.path, item?.parentId === 0 && item?.privilegeIcon ? <img src={`/images/menu-icon-${item?.privilegeIcon}.svg`} /> : '', children)
}

/**
    格式化菜单树,对生成菜单包括子节点都进行排序以及添加图标,最后返回菜单
*/
const getFormatTree = (tree) => {
    const res = [];
    tree?.sort((a, b) => a?.sort - b?.sort)?.forEach((item) => {
      if (item?.children && item.children.length) {
        res.push(getFormatItem(item, getFormatTree(item?.children)?.sort((a, b) => a?.sort - b?.sort)))
      } else res.push(getFormatItem(item))
    })

    return res;
}

/**
    对每个节点递归查找父节点直到没有父节点位置,最后生成每个对象树
    {
    	"menuId": 1,
        "children": [
	    {
		"menuId": 2,
		"children": [
		    "parentId": 1
		    ...
		],
		...
	    }
	],
	...
    }
*/
const getObj = (data, menus) => {
    if (data?.parentId === 0) {
      return data;
    } else {
      const parent = menus.find((childItem) => childItem?.menuId === data?.parentId)
      if (!parent) return null;
      if (parent?.children && parent.children?.length > 0) {
        const curItemExistIndex = parent.children?.findIndex((resItem) => resItem.menuId === data.menuId);
	// 避免重复添加子菜单
        if (curItemExistIndex !== -1) parent.children?.splice(curItemExistIndex, 1, data);
        else parent.children.push(data);
      } else parent.children = [data];
      if (parent?.parentId === 0) {
        return parent;
      } else {
        return getObj(parent, menus)
      }
    }
}

/**
    生成菜单树
*/
const getTree = (menus) => {
    const res = [];
    menus?.forEach((item) => {
      const curData = getObj(item, menus);
      if (curData) {
        const curItemExistIndex = res?.findIndex((resItem) => resItem.menuId === curData.menuId);
        if (curItemExistIndex !== -1) res?.splice(curItemExistIndex, 1, curData);
        else res.push(curData)
      }
    })

    const formatTree = getFormatTree(res)
    setMenus(formatTree)
}

const getMenus = () => {
    // 获取菜单列表
    const menus = permissions?.filter((item) => item?.type === '0')
    // 菜单去重
    let uniqueMenus = Array.from(menus.reduce((map, obj) => {
      if (!map.has(obj.menuId)) map.set(obj.menuId, obj);
      return map;
    }, new Map()).values());

    getTree(uniqueMenus)
}

antd Table拆分单元格

// 返回数据格式
const list = [
    {
	children: [
	    {
		field1: 'xxx',
		...    	
	    },
	    ...	
	],
	field1: 'xxx',
	...    	
    },
    ...
]
/*
    以children中最终子元素作为一行,标记每行的rowSpan以及所属的groupIndex
*/
const formatTableData = (list) => {
  const finalList = []
  let rowIndex = 0;
  list?.forEach((item, itemIndex) => {
    const { children, ...otherItemParams } = item
    const childrenLen = children?.length
    if (childrenLen) {
      children?.forEach((child, childIndex) => {
        rowIndex++;
        finalList.push({
          rowIndex,
          ...child,
          ...otherItemParams,
          rowSpan: childIndex === 0 ? childrenLen : 0,
          groupIndex: itemIndex
        })
      })
    } else {
      rowIndex++;
      finalList.push({ rowIndex, ...otherItemParams, rowSpan: 1, groupIndex: itemIndex })
    }
  })

  return finalList
}

// 最后设置onCell
const columns = [
    {
      title: 'xxx',
      dataIndex: 'xxx',
      key: '',
      onCell: (record) => ({
        rowSpan: record?.rowSpan,
      })
    },
    ...
]
// 如果返回的数据中出现多个维度的,彼此之间没有从属关系的个情况下,比较每个children的长度,哪个子节点多就设为最小行单位
const list = [
    {
	children1: [
	    {
		field1: 'xxx',
		...    	
	    },
	    ...	
	],
	children2: [
	    {
		field1: 'xxx',
		...    	
	    },
	    ...	
	],
	field1: 'xxx',
	...    	
    },
    ...
];

const formatTableData = (list) => {
    const finalList = []
    let rowIndex = 0;
    list?.forEach((item, itemIndex) => {
        const { children1, children2, ...otherItemParams } = item
        const children1Len = children1?.length
        const children2Len = children2?.length
        const array = children1Len >= children2Len ? children1?.map((child, childIndex) => {
            const avgRow = Math.floor(children1Len / children2Len)
            return {
                children1Field1: child?.field1,
                children2Field1: children2?.[Math.floor(childIndex / avgRow)]?.field1,
                children2SingleRow: childIndex % avgRow === 0 ? (Math.floor(childIndex / avgRow) < children2Len ? avgRow : 1) : 0,
            }
        }) : children2?.map((child, childIndex) => {
            	...
            }
        })
        if (array?.length) {
            array?.forEach((arrayItem, arrayIndex) => {
                rowIndex++;
                finalList.push({
                    rowIndex,
                    ...arrayItem,
                    ...otherItemParams,
                    rowSpan: arrayIndex === 0 ? array?.length : 0,
                    groupIndex: itemIndex
                })
            })
        } else {
            rowIndex++;
            finalList.push({ rowIndex, ...otherItemParams, rowSpan: 1, groupIndex: itemIndex })
        }
    })

    return list
}

添加删除单元格就是添加删除子节点

mobx

‌MobX‌是一个简单、可扩展的状态管理库,通过透明的函数响应式编程(TFRP)使得状态管理变得简单且可扩展。它通过观察状态的变化来自动更新组件,而不需要手动编写更新逻辑。‌

‌MobX的核心概念和原理‌

MobX的核心概念包括:

‌‌Observable‌:将数据转变为可观察的,当数据变化时,自动通知依赖的数据。 ‌Action‌:任何用来修改状态的操作,通过action修改状态后,会自动触发相关的更新。 ‌‌Computed‌:根据现有状态计算得出的值,当依赖的状态变化时,自动重新计算。 ‌‌Reaction‌:对状态变化做出的反应,用于执行副作用操作。 MobX通过使用响应式编程,当状态发生变化时,自动通知依赖的数据进行更新。这与‌Redux不同,Redux每次修改状态都需要显式地通知所有依赖的组件进行更新,而MobX可以更高效地处理依赖关系。‌

‌MobX的使用场景和优缺点‌

MobX适用于需要高效状态管理的场景,特别是在React应用中。它的优点包括: ‌上手容易‌:相比Redux等状态管理库,MobX的学习曲线较平缓。 ‌性能优越‌:由于MobX只更新变化的部分,而不是全局重新渲染,因此在大型应用中表现更好。 ‌灵活性高‌:可以组织状态为类或对象,而不是函数式的reducer。

缺点包括: ‌生态不如Redux丰富‌:虽然功能强大,但在一些复杂场景下的解决方案可能不如Redux多样。 ‌兼容性问题‌:不同版本的MobX在语法和支持的浏览器上有所不同,需要特别注意版本兼容性。‌

页面搜索条件,页码查询接口请求等都可以写到mobx中,避免重复请求接口以及可以记住搜索条件避免重复搜索

布局

Grid布局

用于涉及多行多列

// 设置两行三列的网格布局
.parentBox {
    display: grid;
    grid-template-columns: 3fr 1fr;
    grid-template-rows: 1fr 1fr;
    gap: 1rem;
}
// 第一个盒子占两行两列,第二个和第三个盒子分别占一行一列
.childBox1 {
    grid-column: 1 / 3;
    grid-row: 1 / 3;
}
.childBox2 {
    grid-column: 3 / 4;
    grid-row: 1 / 2;
}
.childBox3 {
    grid-column: 3 / 4;
    grid-row: 2 / 3;
}

flex布局

用于涉及一行多列

格式化金额

/**
 * 千分位加逗号
 */
export function formatMoney(value) {
  return value?.toString()?.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

获取选中文本

/**
 * 获取选中文本
 */
export const getSelectedText = () => {
  let selectedText = ''
  if (window.getSelection) {
    selectedText = window.getSelection().toString()
  } else if (document.selection && document.selection.type !== 'Control') {
    selectedText = document.selection.createRange().text
  }
  return selectedText
}

递归平铺路由

/**
 * 平铺路由
 */
export const flattenRoutes = (routes) => {
  let data: any = []
  routes.forEach((item) => {
    if (item.children) {
      data = data.concat(flattenRoutes(item.children))
    } else {
      data.push(item)
    }
  })

  return data
}