同步6/16

This commit is contained in:
Zy
2026-06-01 18:12:58 +08:00
parent 38274ab612
commit 50e7b14fd6
183 changed files with 8956 additions and 4803 deletions

View File

@@ -67,7 +67,7 @@ export function validator(value: string | number, regType: RegType): boolean {
/**
* 车牌验证工具类
* 支持传统燃油、新能源、军警、武警、使馆、临时、港澳等全量车牌
* 支持传统燃油、新能源、军警、武警、使馆、临时、港澳、挂车等全量车牌
*/
export class LicensePlateValidator {
/**
@@ -75,49 +75,37 @@ export class LicensePlateValidator {
*/
private static readonly PROVINCE = '[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]';
/**
* 使领馆前缀
*/
private static readonly EMBASSY_PREFIX = '[使领]';
/**
* 验证传统燃油车牌 (蓝牌/黄牌)
* 格式: 省份 + 字母 + 5位字符
* 格式: 省份 + 字母 + 5位字符 (数字或字母,排除I,O)
* 示例: 京A·12345, 粤B·88888
* 总长7位
*/
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());
if (!reg.test(plate.toUpperCase())) return false;
return true;
}
/**
* 验证新能源车牌 (绿牌)
* 小型: 省份 + 字母 + D/F + 4位数字/字母 或 省份 + 字母 + 5位数字/字母 + D/F
* 大型: 省份 + 字母 + 5位数字/字母 + D/F (通常最后一位是D/F)
* 示例: 京AD12345, 京A12345D, 沪AF12345
* 规则:除了标识位 D/F 外,其余序号位必须为数字 (0-9)
* 小型: 省份 + 字母 + D/F + 5位数字 或 省份 + 字母 + 5位数字 + D/F
* 大型: 省份 + 字母 + 5位数字 + D/F
* 总长8位
*/
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])$`);
// 修正:非D/F位仅限数字
// 1. [省份][A-HJ-NP-Z][D-F][0-9]{5} (D/F在序号第1位,后跟5位数字)
// 2. [省份][A-HJ-NP-Z][0-9]{5}[D-F] (D/F在序号最后1位,前跟5位数字)
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z](?:[D-F][0-9]{5}|[0-9]{5}[D-F])$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证警车车牌
* 格式: 省份 + 字母 + 4位数字 + 警
* 示例: 京A·1234警
* 总长7位
*/
static isPolicePlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9]{4}警$`);
@@ -127,45 +115,47 @@ export class LicensePlateValidator {
/**
* 验证教练车牌
* 格式: 省份 + 字母 + 4位数字 + 学
* 示例: 京A·1234学
* 总长7位
*/
static isCoachPlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9]{4}学$`);
return reg.test(plate.toUpperCase());
}
/**
* 验证挂车车牌
* 格式: 省份 + 字母 + 4位数字 + 挂
* 总长7位
*/
static isTrailerPlate(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后紧跟的可能是省份简称或部门字母
* 总长8位 (WJ+1+5)
*/
static isArmedPolicePlate(plate: string): boolean {
// WJ + (省份简称 | 部门字母) + 5位字符
// 部门字母包括: 内边消水电林通海等,这里简化处理,允许字母或省份
const reg = /^WJ(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]|[A-Z])[0-9A-HJ-NP-Z]{5}$/;
const reg = /^WJ(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]|[A-HJ-NP-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
* 格式: 军种字母 + 字母/数字 + 5位字符
* 总长7位
*/
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
* 格式: 使/领 + 6位数字
* 总长7位
*/
static isEmbassyPlate(plate: string): boolean {
const reg = /^[使领][0-9]{6}$/;
@@ -175,7 +165,7 @@ export class LicensePlateValidator {
/**
* 验证港澳入出境车 (粤Z牌)
* 格式: 粤Z + 4位字符 + 港/澳
* 示例: 粤Z·1234港, 粤Z·1234澳
* 总长7位
*/
static isHKMacauPlate(plate: string): boolean {
const reg = /^粤Z[A-HJ-NP-Z0-9]{4}[港澳]$/;
@@ -185,7 +175,7 @@ export class LicensePlateValidator {
/**
* 验证临时车牌
* 格式: 省份 + 字母 + 5位字符 + 临 / 超
* 示例: 京A·12345临, 沪B·12345超
* 总长8位
*/
static isTemporaryPlate(plate: string): boolean {
const reg = new RegExp(`^${this.PROVINCE}[A-HJ-NP-Z][0-9A-HJ-NP-Z]{5}[临超]$`);
@@ -201,11 +191,17 @@ export class LicensePlateValidator {
const upperPlate = plate.toUpperCase().replace(/[·\s-]/g, ''); // 去除分隔符和空格
// 长度预检查:车牌只能是 7 位或 8 位
if (upperPlate.length !== 7 && upperPlate.length !== 8) {
return false;
}
return (
this.isTraditionalPlate(upperPlate) ||
this.isNewEnergyPlate(upperPlate) ||
this.isPolicePlate(upperPlate) ||
this.isCoachPlate(upperPlate) ||
this.isTrailerPlate(upperPlate) ||
this.isArmedPolicePlate(upperPlate) ||
this.isMilitaryPlate(upperPlate) ||
this.isEmbassyPlate(upperPlate) ||

View File

@@ -28,7 +28,7 @@ export function convertBuildingToTree(buildings: any[]): Tree[] {
const unitTree: Tree = {
label: unit.unitNoName,
value: unit.unitNoId,
type: 'unitNo',
type: 'unitNoId',
children: []
};

View File

@@ -1,3 +1,5 @@
import Big from 'big.js';
/**
* 数字金额转中文大写(支持整数、小数)
* @param num - 待转换的金额(支持数字或字符串格式)
@@ -80,3 +82,96 @@ export function convertToChineseCapital(num: number | string): string {
return result;
}
/**
* 金额加法
* @param a 数值
* @param b 数值
* @returns 保留2位小数字符串
*/
export function moneyAdd(a: number | string = 0, b: number | string = 0): string {
return new Big(a).plus(b).toFixed(2);
}
// 金额减法
export function sub(a, b) {
let r1, r2;
try {
r1 = a.toString().split('.')[1].length;
} catch (e) {
r1 = 0;
}
try {
r2 = b.toString().split('.')[1].length;
} catch (e) {
r2 = 0;
}
const m = Math.pow(10, Math.max(r1, r2));
return (a * m - b * m) / m;
}
// 金额乘法
export function mul(a, b) {
let m = 0;
const s1 = a.toString(),
s2 = b.toString();
try {
m += s1.split('.')[1].length;
} catch (e) {}
try {
m += s2.split('.')[1].length;
} catch (e) {}
return (Number(s1.replace('.', '')) * Number(s2.replace('.', ''))) / Math.pow(10, m);
}
// 金额除法
export function div(a, b) {
let t1 = 0,
t2 = 0;
try {
t1 = a.toString().split('.')[1].length;
} catch (e) {}
try {
t2 = b.toString().split('.')[1].length;
} catch (e) {}
const x = Number(a.toString().replace('.', ''));
const y = Number(b.toString().replace('.', ''));
return (x / y) * Math.pow(10, t2 - t1);
}
// 保留2位小数(金额专用)
export function toFixed2(val) {
return Number(val).toFixed(2);
}
/**
* 智能金额格式化(带单位:元/万元/亿元)
* 兼容:数字、字符串、null、undefined
* @param {number|string} amount 金额
* @returns {string} 格式化后带单位的金额
*/
export function formatMoneyWithUnit(amount) {
// 空值处理
if (amount == null || amount === '') return '0.00 元';
const num = Number(amount);
if (isNaN(num)) return '0.00 元';
// 绝对值判断单位
const absNum = Math.abs(num);
if (absNum >= 100000000) {
// 亿元
return (num / 100000000).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 亿元';
} else if (absNum >= 10000) {
// 万元
return (num / 10000).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 万元';
} else {
// 元
return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ' 元';
}
}
// 带货币符号(¥)版本
export function formatMoneyWithSymbol(amount) {
return '¥' + formatMoneyWithUnit(amount);
}

View File

@@ -261,3 +261,110 @@ export function printDivNewWindow(elementId: string, title: string = '打印') {
}, 300);
};
}
/**
* 🔥 Vue后台专用 - 票据专用打印(固定尺寸 宽10cm 高25cm)
* 新窗口打印指定DIV、还原原样式、屏蔽后台侧边栏/导航、适配Element Plus
* 纸张规格:10cm(宽) × 25cm(高) 票据纸
*/
export function printTicketDiv(elementId: string, title: string = '票据打印', callback?: () => void) {
const el = document.getElementById(elementId);
if (!el) {
console.error('未找到打印元素');
return;
}
const win = window.open('', '_blank', 'width=1200,height=1000');
if (!win) return;
// ==============================================
// 【核心1】复制 全局样式 + Element Plus 样式
// ==============================================
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"], style:not([scoped])'))
.map((item) => item.outerHTML)
.join('');
// ==============================================
// 【核心2】手动注入打印区域的 scoped 样式(关键!)
// ==============================================
const printScopeStyle = `
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
padding: 0.3cm;
background: #fff;
font-size: 12px;
font-family: "Microsoft YaHei", sans-serif;
page-break-inside: avoid !important;
}
/* 隐藏后台布局 */
.el-container, .el-aside, .el-header, .el-footer {
display: none !important;
}
/* ========== 核心:强制 宽25cm 高10cm 无边距 ========== */
@page {
size: 25cm 10cm;
margin: 0;
}
/* 打印容器:固定尺寸,限制单页 */
#${elementId} {
width: 25cm !important;
height: 10cm !important;
margin: 0 auto;
overflow: hidden;
page-break-after: avoid !important;
page-break-inside: avoid !important;
}
/* ========== 表格:压缩高度 + 禁止拆分 ========== */
table {
border-collapse: collapse;
width: 100% !important;
table-layout: fixed;
margin: 0.1cm 0;
page-break-inside: avoid !important;
}
table th, table td {
border: 1px solid #333;
font-size: 10px;
height:40px;
min-width:80px;
text-align: center;
padding: 1px 2px; /* 缩小内边距,减少高度 */
line-height: 1.1;
white-space: normal;
overflow: hidden;
}
</style>
`;
// ==============================================
// 【核心3】写入新窗口
// ==============================================
win.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
<meta charset="UTF-8">
${styles}
${printScopeStyle}
</head>
<body>
${el.outerHTML} <!-- 直接用innerHTML复制完整结构 -->
</body>
</html>
`);
win.document.close();
win.onload = () => {
setTimeout(() => {
win.print();
win.close();
callback && callback();
}, 300);
};
}

View File

@@ -41,10 +41,10 @@ const whiteVillageId = [
'/waterDevice/generateBarCode', // 水表 条码
'/electricityDevice/generateBarCode', // 电表 条码
'/payRecordList/payRecordList/list',
'/warehouse/*'
// '/storeroom/getList/*'
// 以后直接在这里加新地址,支持 /xxx/*、/xxx/xxx 写法
'/warehouse',
'/notification/read', // 已读
'/cash/billList',
'/building/list'
];
/**
@@ -75,7 +75,10 @@ export function needVillageId(url: string): boolean {
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 对应国际化资源文件后缀
const villageid = localStorage.getItem('villageid');
config.headers['Content-Language'] = getLanguage();
config.headers['villageId'] = villageid;
const disableAutoVillage = config.headers?.autoVillage === false;
const isToken = config.headers?.isToken === false;
// 是否需要防止数据重复提交
@@ -87,7 +90,6 @@ service.interceptors.request.use(
config.headers['Authorization'] = 'Bearer ' + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
const villageid = localStorage.getItem('villageid');
if (config.method === 'get' && config.params) {
let url = '';
if (villageid != 'null' && needVillageId(config.url)) {
@@ -95,8 +97,8 @@ service.interceptors.request.use(
config.url +
'?' +
tansParams({
...config.params,
villageId: villageid
villageId: villageid,
...config.params
});
} else {
url = config.url + '?' + tansParams(config.params);
@@ -110,8 +112,8 @@ service.interceptors.request.use(
let obj = null;
if (villageid != 'null' && needVillageId(config.url)) {
obj = {
...config.data,
villageId: villageid
villageId: villageid,
...config.data
};
} else {
obj = config.data;
@@ -166,6 +168,7 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
(res: AxiosResponse) => {
const silentError = res.config.headers?.silentError === 'true';
if (import.meta.env.VITE_APP_ENCRYPT === 'true') {
// 加密后的 AES 秘钥
const keyStr = res.headers[encryptHeader];
@@ -213,7 +216,9 @@ service.interceptors.response.use(
return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
} else if (code === HttpStatus.SERVER_ERROR) {
// 500
ElMessage({ message: msg, type: 'error' });
if (!silentError) {
ElMessage({ message: msg, type: 'error' });
}
return Promise.reject(res.data);
} else if (code === HttpStatus.WARN) {
// 601
@@ -249,7 +254,7 @@ export function download(url: string, params: any, fileName: string) {
return tansParams(params);
}
],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded',silentError: 'true' },
responseType: 'blob'
}).then(async (resp: any) => {
const isLogin = blobValidate(resp);

View File

@@ -261,3 +261,33 @@ export function formatDate(date: Date | string = new Date(), format: string = 'Y
// 正则替换(通用、无需穷举)
return format.replace(/YYYY/g, YYYY).replace(/MM/g, MM).replace(/DD/g, DD).replace(/HH/g, HH).replace(/mm/g, mm).replace(/ss/g, ss);
}
/**
* 获取过去指定天数的那一天的日期字符串
* @param days 过去多少天,0表示今天,1表示昨天,以此类推
* @param format 日期格式化模板,默认 'YYYY-MM-DD'
* @returns 格式化后的日期字符串
* @category 日期工具
* @example
* getPastDayDate(0) // 今天 -> 2023-10-27
* getPastDayDate(1) // 昨天 -> 2023-10-26
* getPastDayDate(7, 'YYYY年MM月DD日') // 7天前 -> 2023年10月20日
*/
export function getPastDayDate(days: number = 0, format: string = 'YYYY-MM-DD'): string {
// 参数校验
if (!Number.isInteger(days) || days < 0) {
console.warn('getPastDayDate: days 必须是大于等于 0 的整数');
return '';
}
const now = new Date();
// 重置时间为 00:00:00,确保计算的是自然日
now.setHours(0, 0, 0, 0);
// 计算目标时间戳
const targetTime = now.getTime() - days * 24 * 60 * 60 * 1000;
const targetDate = new Date(targetTime);
// 复用 formatDate 进行格式化,保持项目内格式统一
return formatDate(targetDate, format);
}