物业代码生成
This commit is contained in:
74
apps/web-antd/src/utils/dict.ts
Normal file
74
apps/web-antd/src/utils/dict.ts
Normal 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,
|
||||
);
|
||||
}
|
80
apps/web-antd/src/utils/encryption/crypto.ts
Normal file
80
apps/web-antd/src/utils/encryption/crypto.ts
Normal 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);
|
||||
}
|
31
apps/web-antd/src/utils/encryption/jsencrypt.ts
Normal file
31
apps/web-antd/src/utils/encryption/jsencrypt.ts
Normal 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);
|
||||
}
|
46
apps/web-antd/src/utils/file/base64Conver.ts
Normal file
46
apps/web-antd/src/utils/file/base64Conver.ts
Normal 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;
|
||||
});
|
||||
}
|
268
apps/web-antd/src/utils/file/download.ts
Normal file
268
apps/web-antd/src/utils/file/download.ts
Normal 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;
|
||||
}
|
31
apps/web-antd/src/utils/file/index.ts
Normal file
31
apps/web-antd/src/utils/file/index.ts
Normal 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 = 1024B,1MB = 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]}`;
|
||||
}
|
64
apps/web-antd/src/utils/modal.tsx
Normal file
64
apps/web-antd/src/utils/modal.tsx
Normal 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',
|
||||
});
|
||||
}
|
126
apps/web-antd/src/utils/popup.ts
Normal file
126
apps/web-antd/src/utils/popup.ts
Normal 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);
|
||||
};
|
||||
}
|
230
apps/web-antd/src/utils/render.tsx
Normal file
230
apps/web-antd/src/utils/render.tsx
Normal 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');
|
||||
}
|
Reference in New Issue
Block a user