物业代码生成

This commit is contained in:
2025-06-18 11:03:42 +08:00
commit 1262d4c745
1881 changed files with 249599 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import { dictDataInfo } from '#/api/system/dict/dict-data';
import { useDictStore } from '#/store/dict';
/**
* 抽取公共逻辑的基础方法
* @param dictName 字典名称
* @param dataGetter 获取字典数据的函数
* @param formatNumber 是否格式化字典value为number类型
* @returns 数据
*/
function fetchAndCacheDictData<T>(
dictName: string,
dataGetter: () => T[],
formatNumber = false,
): T[] {
const { dictRequestCache, setDictInfo } = useDictStore();
// 有调用方决定如何获取数据
const dataList = dataGetter();
// 检查请求状态缓存
if (dataList.length === 0 && !dictRequestCache.has(dictName)) {
dictRequestCache.set(
dictName,
dictDataInfo(dictName)
.then((resp) => {
// 缓存到store 这样就不用重复获取了
// 内部处理了push的逻辑 这里不用push
setDictInfo(dictName, resp, formatNumber);
})
.catch(() => {
// 401时 移除字典缓存 下次登录重新获取
dictRequestCache.delete(dictName);
})
.finally(() => {
// 移除请求状态缓存
/**
* 这里主要判断字典item为空的情况(无奈兼容 不给字典item本来就是错误用法)
* 会导致if一直进入逻辑导致接口无限刷新
* 在这里dictList为空时 不删除缓存
*/
if (dataList.length > 0) {
dictRequestCache.delete(dictName);
}
}),
);
}
return dataList;
}
/**
* 这里是提供给渲染标签使用的方法
* @deprecated 使用getDictOptions代替 于下个版本删除
* @param dictName 字典名称
* @returns 字典信息
*/
export function getDict(dictName: string) {
const { getDictOptions } = useDictStore();
return fetchAndCacheDictData(dictName, () => getDictOptions(dictName));
}
/**
* 一般是Select, Radio, Checkbox等组件使用
* @param dictName 字典名称
* @param formatNumber 是否格式化字典value为number类型
* @returns Options数组
*/
export function getDictOptions(dictName: string, formatNumber = false) {
const { getDictOptions } = useDictStore();
return fetchAndCacheDictData(
dictName,
() => getDictOptions(dictName),
formatNumber,
);
}

View File

@@ -0,0 +1,80 @@
import CryptoJS from 'crypto-js';
function randomUUID() {
const chars = [
...'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
];
const uuid = Array.from({ length: 36 });
let rnd = 0;
let r: number;
for (let i = 0; i < 36; i++) {
if (i === 8 || i === 13 || i === 18 || i === 23) {
uuid[i] = '-';
} else if (i === 14) {
uuid[i] = '4';
} else {
if (rnd <= 0x02)
rnd = Math.trunc(0x2_00_00_00 + Math.random() * 0x1_00_00_00);
r = rnd & 16;
rnd = rnd >> 4;
uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r];
}
}
return uuid.join('').replaceAll('-', '').toLowerCase();
}
/**
* 随机生成aes 密钥
*
* @returns aes 密钥
*/
export function generateAesKey() {
return CryptoJS.enc.Utf8.parse(randomUUID());
}
/**
* base64编码
* @param str
* @returns base64编码
*/
export function encryptBase64(str: CryptoJS.lib.WordArray) {
return CryptoJS.enc.Base64.stringify(str);
}
/**
* 使用公钥加密
* @param message 加密内容
* @param aesKey aesKey
* @returns 使用公钥加密
*/
export function encryptWithAes(
message: string,
aesKey: CryptoJS.lib.WordArray,
) {
const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
}
/**
* 解密base64
*/
export function decryptBase64(str: string) {
return CryptoJS.enc.Base64.parse(str);
}
/**
* 使用密钥对数据进行解密
*/
export function decryptWithAes(
message: string,
aesKey: CryptoJS.lib.WordArray,
) {
const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return decrypted.toString(CryptoJS.enc.Utf8);
}

View File

@@ -0,0 +1,31 @@
// 密钥对生成 http://web.chacuo.net/netrsakeypair
import { useAppConfig } from '@vben/hooks';
import JSEncrypt from 'jsencrypt';
const { rsaPrivateKey, rsaPublicKey } = useAppConfig(
import.meta.env,
import.meta.env.PROD,
);
/**
* 加密
* @param txt 需要加密的数据
* @returns 加密后的数据
*/
export function encrypt(txt: string) {
const instance = new JSEncrypt();
instance.setPublicKey(rsaPublicKey);
return instance.encrypt(txt);
}
/**
* 解密
* @param txt 需要解密的数据
* @returns 解密后的数据
*/
export function decrypt(txt: string) {
const instance = new JSEncrypt();
instance.setPrivateKey(rsaPrivateKey);
return instance.decrypt(txt);
}

View File

@@ -0,0 +1,46 @@
/**
* @description: base64 to blob
*/
export function dataURLtoBlob(base64Buf: string): Blob {
const arr = base64Buf.split(',');
const typeItem = arr[0];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const mime = typeItem!.match(/:(.*?);/)![1];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const bstr = window.atob(arr[1]!);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
u8arr[n] = bstr.codePointAt(n)!;
}
return new Blob([u8arr], { type: mime });
}
/**
* img url to base64
* @param url
*/
export function urlToBase64(url: string, mineType?: string): Promise<string> {
return new Promise((resolve, reject) => {
let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ctx = canvas!.getContext('2d');
const img = new Image();
img.crossOrigin = '';
img.addEventListener('load', () => {
if (!canvas || !ctx) {
// eslint-disable-next-line prefer-promise-reject-errors
return reject();
}
canvas.height = img.height;
canvas.width = img.width;
ctx.drawImage(img, 0, 0);
const dataURL = canvas.toDataURL(mineType || 'image/png');
canvas = null;
resolve(dataURL);
});
img.src = url;
});
}

View File

@@ -0,0 +1,268 @@
import type { VbenFormProps } from '#/adapter/form';
import { $t } from '@vben/locales';
import { cloneDeep, formatDate } from '@vben/utils';
import { message } from 'ant-design-vue';
import { isFunction } from 'lodash-es';
import { dataURLtoBlob, urlToBase64 } from './base64Conver';
/**
*
* @deprecated 无法处理区间选择器数据 请使用commonDownloadExcel
*
* 下载excel文件
* @param [func] axios函数
* @param [fileName] 文件名称 不需要带xlsx后缀
* @param [requestData] 请求参数
* @param [withRandomName] 是否带随机文件名
*
* @return void
*/
export async function downloadExcel(
func: (data?: any) => Promise<Blob>,
fileName: string,
requestData: any = {},
withRandomName = true,
) {
const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);
try {
const data = await func(requestData);
downloadExcelFile(data, fileName, withRandomName);
} catch (error) {
console.error(error);
} finally {
hideLoading();
}
}
/**
* 源码同packages\@core\ui-kit\form-ui\src\components\form-actions.vue
* @param values 表单值
* @param fieldMappingTime 区间选择器 字段映射
* @returns 格式化后的值
*/
function handleRangeTimeValue(
values: Record<string, any>,
fieldMappingTime: VbenFormProps['fieldMappingTime'],
) {
// 需要深拷贝 可能是readonly的
values = cloneDeep(values);
if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) {
return values;
}
fieldMappingTime.forEach(
([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => {
if (startTimeKey && endTimeKey && values[field] === null) {
Reflect.deleteProperty(values, startTimeKey);
Reflect.deleteProperty(values, endTimeKey);
// delete values[startTimeKey];
// delete values[endTimeKey];
}
if (!values[field]) {
Reflect.deleteProperty(values, field);
// delete values[field];
return;
}
const [startTime, endTime] = values[field];
if (format === null) {
values[startTimeKey] = startTime;
values[endTimeKey] = endTime;
} else if (isFunction(format)) {
values[startTimeKey] = format(startTime, startTimeKey);
values[endTimeKey] = format(endTime, endTimeKey);
} else {
const [startTimeFormat, endTimeFormat] = Array.isArray(format)
? format
: [format, format];
values[startTimeKey] = startTime
? formatDate(startTime, startTimeFormat)
: undefined;
values[endTimeKey] = endTime
? formatDate(endTime, endTimeFormat)
: undefined;
}
// delete values[field];
Reflect.deleteProperty(values, field);
},
);
return values;
}
export interface DownloadExcelOptions {
// 是否随机文件名(带时间戳)
withRandomName?: boolean;
// 区间选择器 字段映射
fieldMappingTime?: VbenFormProps['fieldMappingTime'];
}
/**
* 通用下载excel方法
* @param api 后端下载接口
* @param fileName 文件名 不带拓展名
* @param requestData 请求参数
* @param options 下载选项
*/
export async function commonDownloadExcel(
api: (data?: any) => Promise<Blob>,
fileName: string,
requestData: any = {},
options: DownloadExcelOptions = {},
) {
const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);
try {
const { withRandomName = true, fieldMappingTime } = options;
// 需要处理时间字段映射
const data = await api(handleRangeTimeValue(requestData, fieldMappingTime));
downloadExcelFile(data, fileName, withRandomName);
} catch (error) {
console.error(error);
} finally {
hideLoading();
}
}
export function downloadExcelFile(
data: BlobPart,
filename: string,
withRandomName = true,
) {
let realFileName = filename;
if (withRandomName) {
realFileName = `${filename}-${Date.now()}.xlsx`;
}
downloadByData(data, realFileName);
}
/**
* Download online pictures
* @param url
* @param filename
* @param mime
* @param bom
*/
export function downloadByOnlineUrl(
url: string,
filename: string,
mime?: string,
bom?: BlobPart,
) {
urlToBase64(url).then((base64) => {
downloadByBase64(base64, filename, mime, bom);
});
}
/**
* Download pictures based on base64
* @param buf
* @param filename
* @param mime
* @param bom
*/
export function downloadByBase64(
buf: string,
filename: string,
mime?: string,
bom?: BlobPart,
) {
const base64Buf = dataURLtoBlob(buf);
downloadByData(base64Buf, filename, mime, bom);
}
/**
* Download according to the background interface file stream
* @param {*} data
* @param {*} filename
* @param {*} mime
* @param {*} bom
*/
export function downloadByData(
data: BlobPart,
filename: string,
mime?: string,
bom?: BlobPart,
) {
const blobData = bom === undefined ? [data] : [bom, data];
const blob = new Blob(blobData, { type: mime || 'application/octet-stream' });
const blobURL = window.URL.createObjectURL(blob);
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href = blobURL;
tempLink.setAttribute('download', filename);
if (tempLink.download === undefined) {
tempLink.setAttribute('target', '_blank');
}
document.body.append(tempLink);
tempLink.click();
tempLink.remove();
window.URL.revokeObjectURL(blobURL);
}
export function openWindow(
url: string,
opt?: {
noopener?: boolean;
noreferrer?: boolean;
target?: '_blank' | '_self' | string;
},
) {
const { noopener = true, noreferrer = true, target = '__blank' } = opt || {};
const feature: string[] = [];
noopener && feature.push('noopener=yes');
noreferrer && feature.push('noreferrer=yes');
window.open(url, target, feature.join(','));
}
/**
* Download file according to file address
* @param {*} sUrl
*/
export function downloadByUrl({
fileName,
target = '_blank',
url,
}: {
fileName?: string;
target?: '_blank' | '_self';
url: string;
}): boolean {
const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');
const isSafari = window.navigator.userAgent.toLowerCase().includes('safari');
if (/iP/.test(window.navigator.userAgent)) {
console.error('Your browser does not support download!');
return false;
}
if (isChrome || isSafari) {
const link = document.createElement('a');
link.href = url;
link.target = target;
if (link.download !== undefined) {
link.download =
// eslint-disable-next-line unicorn/prefer-string-slice
fileName || url.substring(url.lastIndexOf('/') + 1, url.length);
}
if (document.createEvent) {
const e = document.createEvent('MouseEvents');
e.initEvent('click', true, true);
link.dispatchEvent(e);
return true;
}
}
if (!url.includes('?')) {
url += '?download';
}
openWindow(url, { target });
return true;
}

View File

@@ -0,0 +1,31 @@
/**
* 计算文件大小并以适当单位表示
*
* 此函数接收一个表示文件大小的数字(以字节为单位),并返回一个格式化后的字符串,
* 该字符串表示文件的大小以最适合的单位B, KB, MB, GB, TB表示
*
* @param size 文件大小,以字节为单位
* @param isInteger 是否返回整数大小默认为false如果设置为true
* 则返回的大小将不包含小数部分如果为false则根据单位的不同
* 返回最多3位小数的大小
* @returns 格式化后的文件大小字符串,如"4.5KB"或"3MB"
*/
export function calculateFileSize(size: number, isInteger = false) {
// 定义文件大小的单位数组
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
// 定义换算基数1KB = 1024B1MB = 1024KB以此类推
const base = 1024;
// 初始化单位索引初始值为0即默认单位为B
let unitIndex = 0;
// 当文件大小大于等于基数且单位索引未超出单位数组范围时,循环进行单位转换
while (size >= base && unitIndex < units.length - 1) {
size /= base;
unitIndex++;
}
// 根据是否需要整数大小,确定输出的精度
const precision = isInteger ? 0 : Math.min(unitIndex, 3);
// 返回格式化后的文件大小字符串
return `${size.toFixed(precision)}${units[unitIndex]}`;
}

View File

@@ -0,0 +1,64 @@
import type { ModalFuncProps } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import { reactive } from 'vue';
import { Alert, Form, Input, Modal } from 'ant-design-vue';
import { isFunction } from 'lodash-es';
export interface ConfirmModalProps extends Omit<ModalFuncProps, 'visible'> {
confirmText?: string;
placeholder?: string;
onValidated?: () => Promise<void>;
}
export function confirmDeleteModal(props: ConfirmModalProps) {
const placeholder = props.placeholder || `输入'确认删除'`;
const confirmText = props.confirmText || '确认删除';
const formValue = reactive({
content: '',
});
const rulesRef = reactive<{ [key: string]: Rule[] }>({
content: [
{
message: '校验不通过',
required: true,
trigger: 'change',
validator(_, value) {
if (value !== confirmText) {
return Promise.reject(new Error('校验不通过'));
}
return Promise.resolve();
},
},
],
});
const useForm = Form.useForm;
const { validate, validateInfos } = useForm(formValue, rulesRef);
Modal.confirm({
...props,
centered: true,
content: (
<div class="flex flex-col gap-[8px]">
<Alert message={'确认删除后将无法恢复,请谨慎操作!'} type="error" />
<Form layout="vertical" model={formValue}>
<Form.Item {...validateInfos.content}>
<Input
placeholder={placeholder}
v-model:value={formValue.content}
/>
</Form.Item>
</Form>
</div>
),
okButtonProps: { danger: true, type: 'primary' },
onOk: async () => {
await validate();
isFunction(props.onValidated) && props.onValidated();
},
title: '提示',
type: 'warning',
});
}

View File

@@ -0,0 +1,126 @@
import type { ExtendedFormApi } from '@vben/common-ui';
import type { MaybePromise } from '@vben/types';
import { ref } from 'vue';
import { $t } from '@vben/locales';
import { Modal } from 'ant-design-vue';
import { isFunction } from 'lodash-es';
interface BeforeCloseDiffProps {
/**
* 初始化值如何获取
* @returns Promise<string>
*/
initializedGetter: () => MaybePromise<string>;
/**
* 当前值如何获取
* @returns Promise<string>
*/
currentGetter: () => MaybePromise<string>;
/**
* 自定义比较函数
* @param init 初始值
* @param current 当前值
* @returns boolean
*/
compare?: (init: string, current: string) => boolean;
}
/**
* 用于Drawer/Modal使用 判断表单是否有变动来决定是否弹窗提示
* @param props props
* @returns hook
*/
export function useBeforeCloseDiff(props: BeforeCloseDiffProps) {
const { initializedGetter, currentGetter, compare } = props;
/**
* 记录初始值 json
*/
const initialized = ref<string>('');
/**
* 是否已经初始化了 通过这个值判断是否需要进行对比 为false直接关闭 不弹窗
*/
const isInitialized = ref(false);
/**
* 标记是否已经完成初始化 后续需要进行对比
* @param data 自定义初始化数据 可选
*/
async function markInitialized(data?: string) {
initialized.value = data || (await initializedGetter());
isInitialized.value = true;
}
/**
* 重置初始化状态 需要在closed前调用 或者打开窗口时
*/
function resetInitialized() {
initialized.value = '';
isInitialized.value = false;
}
/**
* 提供给useVbenForm/useVbenDrawer使用
* @returns 是否允许关闭
*/
async function onBeforeClose(): Promise<boolean> {
// 如果还未初始化,直接允许关闭
if (!isInitialized.value) {
return true;
}
try {
// 获取当前表单数据
const current = await currentGetter();
// 自定义比较的情况
if (isFunction(compare) && compare(initialized.value, current)) {
return true;
} else {
// 如果数据没有变化,直接允许关闭
if (current === initialized.value) {
return true;
}
}
// 数据有变化,显示确认对话框
return new Promise<boolean>((resolve) => {
Modal.confirm({
title: $t('pages.common.tip'),
content: $t('pages.common.beforeCloseTip'),
centered: true,
okButtonProps: { danger: true },
cancelText: $t('common.cancel'),
okText: $t('common.confirm'),
onOk: () => {
resolve(true);
isInitialized.value = false;
},
onCancel: () => resolve(false),
});
});
} catch (error) {
console.error('Failed to compare data:', error);
return true;
}
}
return {
onBeforeClose,
markInitialized,
resetInitialized,
};
}
/**
* 给useVbenForm使用的 封装函数
* @param formApi 表单实例
* @returns getter
*/
export function defaultFormValueGetter(formApi: ExtendedFormApi) {
return async () => {
const v = await formApi.getValues();
return JSON.stringify(v);
};
}

View File

@@ -0,0 +1,230 @@
import type { Component as ComponentType } from 'vue';
import type { DictData } from '#/api/system/dict/dict-data-model';
import { h } from 'vue';
import { JsonPreview } from '@vben/common-ui';
import {
AndroidIcon,
BaiduIcon,
ChromeIcon,
DefaultBrowserIcon,
DefaultOsIcon,
DingtalkIcon,
EdgeIcon,
FirefoxIcon,
IconifyIcon,
IPhoneIcon,
LinuxIcon,
MicromessengerIcon,
OperaIcon,
OSXIcon,
QuarkIcon,
SafariIcon,
SvgQQIcon,
UcIcon,
WindowsIcon,
} from '@vben/icons';
import { Tag } from 'ant-design-vue';
import { DictTag } from '#/components/dict';
import { getDictOptions } from './dict';
/**
* 渲染标签
* @param text 文字
* @param color 颜色
* @returns render
*/
function renderTag(text: string, color?: string) {
return <Tag color={color}>{text}</Tag>;
}
/**
*
* @param tags 标签list
* @param wrap 是否换行显示
* @param [gap] 间隔
* @returns render
*/
export function renderTags(tags: string[], wrap = false, gap = 1) {
return (
<div
class={['flex', wrap ? 'flex-col' : 'flex-row']}
style={{ gap: `${gap}px` }}
>
{tags.map((tag, index) => {
return <div key={index}>{renderTag(tag)}</div>;
})}
</div>
);
}
/**
*
* @param json json对象 接受object/string类型
* @returns json预览
*/
export function renderJsonPreview(json: any) {
if (typeof json !== 'object' && typeof json !== 'string') {
return <span>{json}</span>;
}
if (typeof json === 'object') {
return <JsonPreview class="break-normal" data={json} />;
}
try {
const obj = JSON.parse(json);
// 基本数据类型可以被转为json
if (typeof obj !== 'object') {
return <span>{obj}</span>;
}
return <JsonPreview class="break-normal" data={obj} />;
} catch {
return <span>{json}</span>;
}
}
/**
* iconify图标
* @param icon icon名称
* @returns render
*/
export function renderIcon(icon: string) {
return <IconifyIcon icon={icon}></IconifyIcon>;
}
/**
* httpMethod标签
* @param type method类型
* @returns render
*/
export function renderHttpMethodTag(type: string) {
const method = type.toUpperCase();
const colors: { [key: string]: string } = {
DELETE: 'red',
GET: 'green',
POST: 'blue',
PUT: 'orange',
};
const color = colors[method] ?? 'default';
const title = `${method}请求`;
return <Tag color={color}>{title}</Tag>;
}
export function renderDictTag(value: number | string, dicts: DictData[]) {
return <DictTag dicts={dicts} value={value}></DictTag>;
}
/**
* render多个dictTag
* @param value key数组 string[]类型
* @param dicts 字典数组
* @param wrap 是否需要换行显示
* @param [gap] 间隔
* @returns render
*/
export function renderDictTags(
value: string[],
dicts: DictData[],
wrap = true,
gap = 1,
) {
if (!Array.isArray(value)) {
return <div>{value}</div>;
}
return (
<div
class={['flex', wrap ? 'flex-col' : 'flex-row']}
style={{ gap: `${gap}px` }}
>
{value.map((item, index) => {
return <div key={index}>{renderDictTag(item, dicts)}</div>;
})}
</div>
);
}
/**
* 显示字典标签 一般是table使用
* @param value 值
* @param dictName dictName
* @returns tag
*/
export function renderDict(value: number | string, dictName: string) {
const dictInfo = getDictOptions(dictName);
return renderDictTag(value, dictInfo);
}
export function renderIconSpan(
icon: ComponentType,
value: string,
center = false,
marginLeft = '2px',
) {
const justifyCenter = center ? 'justify-center' : '';
return (
<span class={['flex', 'items-center', justifyCenter]}>
{h(icon)}
<span style={{ marginLeft }}>{value}</span>
</span>
);
}
const osOptions = [
{ icon: WindowsIcon, value: 'windows' },
{ icon: LinuxIcon, value: 'linux' },
{ icon: OSXIcon, value: 'osx' },
{ icon: AndroidIcon, value: 'android' },
{ icon: IPhoneIcon, value: 'iphone' },
];
/**
* 浏览器图标
* cn.hutool.http.useragent -> browers
*/
const browserOptions = [
{ icon: ChromeIcon, value: 'chrome' },
{ icon: EdgeIcon, value: 'edge' },
{ icon: FirefoxIcon, value: 'firefox' },
{ icon: OperaIcon, value: 'opera' },
{ icon: SafariIcon, value: 'safari' },
{ icon: MicromessengerIcon, value: 'micromessenger' },
{ icon: MicromessengerIcon, value: 'windowswechat' },
{ icon: QuarkIcon, value: 'quark' },
{ icon: MicromessengerIcon, value: 'wxwork' },
{ icon: SvgQQIcon, value: 'qq' },
{ icon: DingtalkIcon, value: 'dingtalk' },
{ icon: UcIcon, value: 'uc' },
{ icon: BaiduIcon, value: 'baidu' },
];
export function renderOsIcon(os: string, center = false) {
if (!os) {
return;
}
let current = osOptions.find((item) =>
os.toLocaleLowerCase().includes(item.value),
);
// windows要特殊处理
if (os.toLocaleLowerCase().includes('windows')) {
current = osOptions[0];
}
const icon = current ? current.icon : DefaultOsIcon;
return renderIconSpan(icon, os, center, '5px');
}
export function renderBrowserIcon(browser: string, center = false) {
if (!browser) {
return;
}
const current = browserOptions.find((item) =>
browser.toLocaleLowerCase().includes(item.value),
);
const icon = current ? current.icon : DefaultBrowserIcon;
return renderIconSpan(icon, browser, center, '5px');
}