修正ts类型

This commit is contained in:
Zy
2026-05-13 15:28:25 +08:00
parent 1b738c0f4d
commit a1750bc67b
114 changed files with 2346 additions and 613 deletions

View File

@@ -64,3 +64,153 @@ export type RegType = keyof typeof reg;
export function validator(value: string | number, regType: RegType): boolean {
return reg[regType].test(String(value));
}
/**
* 车牌验证工具类
* 支持传统燃油、新能源、军警、武警、使馆、临时、港澳等全量车牌
*/
export class LicensePlateValidator {
/**
* 省份简称正则片段
*/
private static readonly PROVINCE = '[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]';
/**
* 使领馆前缀
*/
private static readonly EMBASSY_PREFIX = '[使领]';
/**
* 验证传统燃油车牌 (蓝牌/黄牌)
* 格式: 省份 + 字母 + 5位字符
* 示例: 京A·12345, 粤B·88888
*/
static isTraditionalPlate(plate: string): boolean {
// 包含普通民用以及使领馆非新能源部分(如果使领馆归为此类,通常使领馆有单独规则,这里仅指普通民用)
// 注意:使领馆车牌通常格式特殊,见 isEmbassyPlate
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][A-HJ-NP-Z0-9]{5}$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证新能源车牌 (绿牌)
* 小型: 省份 + 字母 + D/F + 4位数字/字母 或 省份 + 字母 + 5位数字/字母 + D/F
* 大型: 省份 + 字母 + 5位数字/字母 + D/F (通常最后一位是D/F)
* 示例: 京AD12345, 京A12345D, 沪AF12345
*/
static isNewEnergyPlate(plate: string): boolean {
// 小型新能源第2位或第7位是D/F
// 大型新能源最后一位是D/F (且总长8位)
// 综合正则:
// 1. [省份][A-HJ-NP-Z][D-F][0-9A-HJ-NP-Z]{4} (小型D/F在第2位)
// 2. [省份][A-HJ-NP-Z][0-9A-HJ-NP-Z]{4}[D-F] (小型/大型D/F在最后)
// 3. [省份][A-HJ-NP-Z][0-9A-HJ-NP-Z][D-F][0-9A-HJ-NP-Z]{3} (较少见但某些早期试点或特殊编排可能存在标准通常只认1和2)
// 更严谨的标准新能源正则 (GA 804-2018):
// 小型: [省份][A-HJ-NP-Z][D-F][0-9A-HJ-NP-Z]{4} | [省份][A-HJ-NP-Z][0-9A-HJ-NP-Z]{4}[D-F]
// 大型: [省份][A-HJ-NP-Z][0-9A-HJ-NP-Z]{5}[D-F] (其实包含在上面第二种情况的子集里只要保证总长8位)
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z](?:[D-F][0-9A-HJ-NP-Z]{4}|[0-9A-HJ-NP-Z]{5}[D-F])$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证警车车牌
* 格式: 省份 + 字母 + 4位数字 + 警
* 示例: 京A·1234警
*/
static isPolicePlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9]{4}警$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证教练车牌
* 格式: 省份 + 字母 + 4位数字 + 学
* 示例: 京A·1234学
*/
static isCoachPlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9]{4}学$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证武警车牌 (2013式)
* 格式: WJ + 省份/部门简称 + 5位字符
* 示例: WJ·京12345, WJ·12345X, WJ·B12345
* 注意WJ后紧跟的可能是省份简称或部门字母
*/
static isArmedPolicePlate(plate: string): boolean {
// WJ + (省份简称 | 部门字母) + 5位字符
// 部门字母包括: 内边消水电林通海等,这里简化处理,允许字母或省份
const reg = /^WJ(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]|[A-Z])[0-9A-HJ-NP-Z]{5}$/;
return reg.test(plate.toUpperCase());
}
/**
* 验证军队车牌 (2012式)
* 格式: 军种字母 + · + 5位字符 (实际输入常不带点)
* 军种: V(军委), K(空军), H(海军), B(北京军区), S(沈阳), L(兰州), J(济南), N(南京), G(广州), C(成都) 等
* 示例: V·12345, K·A1234
*/
static isMilitaryPlate(plate: string): boolean {
// 简化版首字母为大写排除I,O后跟5位字符
// 更严谨应限制首字母为特定军种代号,但考虑到军改变化,此处做通用校验
// 2012式军牌格式XY·12345 (X为军种/大区Y为单位)
// 常见前缀: V, K, H, B, S, L, J, N, G, C, M, P, O, A, E 等
const reg = /^[VKBHSLJNGCMPEOA][A-HJ-NP-Z0-9][0-9A-HJ-NP-Z]{4}$/;
return reg.test(plate.toUpperCase());
}
/**
* 验证使馆/领馆车牌
* 格式: 使/领 + 3位数字 + 3位数字 (共6位数字或 使·123456)
* 示例: 使·123456, 领·123456
*/
static isEmbassyPlate(plate: string): boolean {
const reg = /^[使领][0-9]{6}$/;
return reg.test(plate);
}
/**
* 验证港澳入出境车 (粤Z牌)
* 格式: 粤Z + 4位字符 + 港/澳
* 示例: 粤Z·1234港, 粤Z·1234澳
*/
static isHKMacauPlate(plate: string): boolean {
const reg = /^粤Z[A-HJ-NP-Z0-9]{4}[港澳]$/;
return reg.test(plate.toUpperCase());
}
/**
* 验证临时车牌
* 格式: 省份 + 字母 + 5位字符 + 临 / 超
* 示例: 京A·12345临, 沪B·12345超
*/
static isTemporaryPlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9A-HJ-NP-Z]{5}[临超]$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证所有合法车牌(全量)
* @param plate 车牌号码
*/
static isValidPlate(plate: string): boolean {
if (!plate || typeof plate !== 'string') return false;
const upperPlate = plate.toUpperCase().replace(/[·\s-]/g, ''); // 去除分隔符和空格
return (
this.isTraditionalPlate(upperPlate) ||
this.isNewEnergyPlate(upperPlate) ||
this.isPolicePlate(upperPlate) ||
this.isCoachPlate(upperPlate) ||
this.isArmedPolicePlate(upperPlate) ||
this.isMilitaryPlate(upperPlate) ||
this.isEmbassyPlate(upperPlate) ||
this.isHKMacauPlate(upperPlate) ||
this.isTemporaryPlate(upperPlate)
);
}
}

117
src/utils/exportZip.ts Normal file
View File

@@ -0,0 +1,117 @@
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
// 假设使用的是 Element Plus请根据实际 UI 库调整导入
import { ElMessage } from 'element-plus';
/**
* 图片数据接口
*/
interface ImageItem {
name: string;
url: string;
}
/**
* 批量导出图片并生成 ZIP
* @param imageList 图片数组
* @param zipName 压缩包名称
* @param concurrency 并发请求数,默认 5避免浏览器请求过多被拦截
*/
export async function exportImagesToZip(imageList: ImageItem[], zipName: string = 'images', concurrency: number = 5) {
if (!imageList || imageList.length === 0) {
ElMessage.warning('没有可导出的图片');
return;
}
const zip = new JSZip();
try {
// 使用并发控制处理图片获取
const tasks = imageList.map((item) => () => processImageItem(item, zip));
// 执行并发任务
await runWithConcurrency(tasks, concurrency);
ElMessage.success('导出完成');
// 生成 ZIP 并下载
const content = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
});
saveAs(content, `${zipName}.zip`);
} catch (err) {
console.error('ZIP 导出失败:', err);
ElMessage.error('图片导出失败,请重试');
}
}
/**
* 处理单个图片项:获取 Blob 并添加到 Zip
*/
async function processImageItem(item: ImageItem, zip: JSZip): Promise<void> {
try {
// 清理文件名,防止非法字符或扩展名缺失
const safeName = sanitizeFileName(item.name);
const blob = await getImageBlobViaCanvas(item.url);
zip.file(`${safeName}.png`, blob, { binary: true });
} catch (error) {
console.warn(`图片加载失败: ${item.url}`, error);
throw error; // 抛出错误以便外层统计或处理
}
}
/**
* 通过 Canvas 获取图片 Blob (解决 OSS 跨域 fetch 问题)
* 注意:这要求 OSS _bucket_ 配置了 CORS 允许 '*' 或你的域名,且允许 GET 方法。
* 如果 OSS 完全禁止跨域,此方法也会失败,但通常公共读 OSS 是允许 img 标签跨域显示的。
*/
async function getImageBlobViaCanvas(url: string): Promise<Blob> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
}
/**
* 简单的并发控制执行器
*/
async function runWithConcurrency(tasks: (() => Promise<void>)[], limit: number): Promise<void> {
const results: Promise<void>[] = [];
let index = 0;
async function worker() {
while (index < tasks.length) {
const currentIndex = index++;
const task = tasks[currentIndex];
try {
await task();
} catch (e) {
// 这里可以选择忽略单个失败,或者累计错误
console.error(`Task ${currentIndex} failed`, e);
}
}
}
// 创建 limit 个工人
const workers = Array(Math.min(limit, tasks.length)).fill(null).map(worker);
await Promise.all(workers);
}
/**
* 清理文件名,确保合法且唯一(简单版)
*/
function sanitizeFileName(name: string): string {
// 移除非法字符,保留字母、数字、中文、下划线、点、横杠
let cleanName = name.replace(/[<>:"/\\|?*]/g, '_');
// 如果名字为空或只有扩展名,给一个默认名
if (!cleanName || cleanName.startsWith('.')) {
cleanName = `image_${Date.now()}_${Math.random().toString(36).substr(2, 9)}${cleanName}`;
}
return cleanName;
}

View File

@@ -58,22 +58,6 @@ export function convertBuildingToTree(buildings: any[]): Tree[] {
});
}
// 与业主关系
export const ownerRelationshiplist = [
{
label: '业主',
value: 0
},
{
label: '亲属',
value: 1
},
{
label: '租户',
value: 2
}
];
interface Tree {
label: string;
value: number;
@@ -120,8 +104,6 @@ export function getHousePathById(treeData: Tree[], houseId: string | number, res
return path;
}
export const houseUselist = ['普通住宅', '商业用地', '商住两用', '其他'];
// 对字符串 图片地址,处理
export function splitStringUrl(str: string, splitLabel: string = ',') {
const list = str.split(splitLabel);
@@ -149,25 +131,3 @@ export function hasFormData(formData: FormData): boolean {
}
return false;
}
/** 方法渲燃模板数据 */
type dataType = {
'label': string;
'value': string;
'elTagType': string;
'elTagClass': string;
};
/**
* 通用:根据 value 从字典数组中获取 label
* @param dict 字典数组
* @param value 要匹配的值
* @returns 匹配到的label无则返回原value
*/
export function getDictLabel(dict: Ref<dataType>, value: number): string {
// 空值直接返回
if (value == null) return '-';
// 统一转字符串匹配
const item = (dict.value || []).find((i) => String(i.value) === String(value));
return item ? item.label : String(value);
}

View File

@@ -34,14 +34,15 @@ const service = axios.create({
clarifyTimeoutError: true
}
});
// 不需要小区 ID 的接口白名单(支持 * 通配符)
const whiteVillageId = [
'/village',
'/village/*',
'/waterDevice/generateBarCode', // 水表 条码
'/electricityDevice/generateBarCode', // 电表 条码
'/payRecordList/payRecordList/list'
'/payRecordList/payRecordList/list',
'/warehouse/*'
// 以后直接在这里加新地址,支持 /xxx/*、/xxx/xxx 写法
];
@@ -202,9 +203,6 @@ service.interceptors.response.use(
useUserStore().logout().then(() => {
router.replace({
path: '/login',
query: {
redirect: encodeURIComponent(router.currentRoute.value.fullPath || '/')
}
})
});
}).catch(() => {

198
src/utils/weather.ts Normal file
View File

@@ -0,0 +1,198 @@
const weatherIcons: Record<number, string> = {
100: '☀️',
101: '⛅',
102: '🌤️',
103: '⛅',
104: '☁️',
150: '🌙',
151: '🌙',
152: '🌙',
153: '🌙',
300: '🌦️',
301: '🌧️',
302: '⛈️',
303: '⛈️',
304: '⛈️',
305: '🌧️',
306: '🌧️',
307: '🌧️',
308: '🌧️',
309: '🌧️',
310: '🌊',
311: '🌊',
312: '🌊',
313: '🧊',
314: '🌧️',
315: '🌧️',
316: '🌊',
317: '🌊',
318: '🌊',
350: '🌙',
351: '🌙',
399: '🌧️',
400: '🌨️',
401: '🌨️',
402: '❄️',
403: '❄️',
404: '🌨️',
405: '🌨️',
406: '🌨️',
407: '🌨️',
408: '🌨️',
409: '❄️',
410: '❄️',
456: '🌙',
457: '🌙',
499: '❄️',
500: '🌫️',
501: '🌫️',
502: '😶‍🌫️',
503: '💨',
504: '💨',
507: '🏜️',
508: '🏜️',
509: '🌫️',
510: '🌫️',
511: '😶‍🌫️',
512: '😶‍🌫️',
513: '😶‍🌫️',
514: '🌫️',
515: '🌫️',
800: '🌑',
801: '🌒',
802: '🌓',
803: '🌔',
804: '🌕',
805: '🌖',
806: '🌗',
807: '🌘',
900: '🥵',
901: '🥶',
999: '❓',
1001: '🌀',
1002: '🌪️',
1003: '🌊',
1004: '❄️',
1005: '🥶',
1006: '💨',
1007: '🏜️',
1008: '🧊',
1009: '🌡️',
1010: '🥵',
1011: '🌾',
1012: '💨',
1013: '🏔️',
1014: '⚡',
1015: '🧊',
1016: '🥶',
1017: '🌫️',
1018: '✈️',
1019: '😶‍🌫️',
1020: '⛈️',
1021: '🧊',
1022: '☀️',
1023: '🌊',
1024: '🥵',
1025: '🔥',
1026: '🔥',
1027: '🧊',
1028: '🛰️',
1029: '😶‍🌫️',
1030: '🌨️',
1031: '⛈️',
1032: '☀️',
1033: '❄️',
1034: '🥶',
1035: '🌧️',
1036: '🌊',
1037: '⛰️',
1038: '🌧️',
1039: '🌡️',
1040: '❄️',
1041: '🔥',
1042: '🏥',
1043: '⛈️',
1044: '🏫',
1045: '🏭',
1046: '🚢',
1047: '💨',
1048: '🌡️',
1049: '🌀',
1050: '🥶',
1051: '💨',
1052: '⛈️',
1053: '🌫️',
1054: '⚡',
1055: '🌀',
1056: '🌡️',
1057: '🧊',
1058: '⛈️',
1059: '🥶',
1060: '🌫️',
1061: '💨',
1062: '🌊',
1063: '🌧️',
1064: '🌧️',
1065: '🌫️',
1066: '🥵',
1067: '😶‍🌫️',
1068: '⚠️',
1069: '🏥',
1071: '🏥',
1072: '🏥',
1073: '🌊',
1074: '😶‍🌫️',
1075: '🏙️',
1076: '🌊',
1077: '🔥',
1078: '☀️',
1079: '🌾',
1080: '💨',
1081: '🧊',
1082: '🏥',
1084: '🔥',
1085: '⛈️',
1086: '🧊',
1087: '🌡️',
1088: '🌾',
1089: '🌾',
1201: '🌊',
1202: '🏙️',
1203: '⚠️',
1204: '⚠️',
1205: '🧊',
1206: '🌊',
1207: '🌊',
1208: '☀️',
1209: '🌊',
1210: '💧',
1211: '🌊',
1212: '🌊',
1213: '🏙️',
1214: '🌊',
1215: '☀️',
1216: '💧',
1217: '🌿',
1218: '⚠️',
1219: '🌊',
1221: '☀️',
1241: '⛰️',
1242: '⛰️',
1243: '⛰️',
1244: '⛰️',
1245: '⛰️',
1246: '⛰️',
1247: '🌋',
1248: '⛰️',
1249: '⛰️',
1250: '⛰️',
1251: '⛰️',
1271: '😶‍🌫️',
1272: '😶‍🌫️',
1273: '😶‍🌫️',
9999: '⚠️'
};
export const getWeatherIcon = (weatherCode: string): string => {
return weatherIcons[weatherCode] || weatherIcons[999];
};