7/30 限制前端输入字数
This commit is contained in:
@@ -51,7 +51,11 @@ const reg = {
|
||||
/** QQ */
|
||||
qq: /^[1-9]\d{4,11}$/,
|
||||
/** 微信 */
|
||||
wechat: /^[a-zA-Z][a-zA-Z0-9_-]{5,19}$/
|
||||
wechat: /^[a-zA-Z][a-zA-Z0-9_-]{5,19}$/,
|
||||
/** 护照 */
|
||||
passport: /^[EG]\d{8}$/,
|
||||
/** 港澳通行证 */
|
||||
hongkongPassport: /^[CHMW]\d{8}$/
|
||||
};
|
||||
// 自动推导 TS 类型(最标准)
|
||||
export type RegType = keyof typeof reg;
|
||||
|
||||
699
src/utils/Reg2.ts
Normal file
699
src/utils/Reg2.ts
Normal file
@@ -0,0 +1,699 @@
|
||||
/**
|
||||
* 中国车牌号校验工具
|
||||
*
|
||||
* 支持的车牌类型:
|
||||
* 1. 普通蓝牌/黄牌(7位)— 小型汽车 / 大型汽车 / 摩托车
|
||||
* 2. 新能源小车(8位)— 纯电动(D) / 非纯电动(F)
|
||||
* 3. 新能源大车(8位)— 纯电动(D) / 非纯电动(F)
|
||||
* 4. 警车牌(7位)
|
||||
* 5. 教练车牌(7位)
|
||||
* 6. 挂车牌(7位)
|
||||
* 7. 港澳入出境车牌(7位)
|
||||
* 8. 临时行驶车牌(8位)
|
||||
* 9. 使馆车牌
|
||||
* 10. 领事馆车牌(7位)
|
||||
* 11. 武警车牌
|
||||
* 12. 试车牌(7位)
|
||||
*
|
||||
* 合理性校验规则:
|
||||
* - 字母数字混合段至少包含 1 位数字,拒绝纯字母组合(如京AAAAAA)
|
||||
* - 字母排除 I、O(避免与数字 1、0 混淆)
|
||||
*/
|
||||
|
||||
// ===================== 常量 =====================
|
||||
|
||||
/** 省份简称(31个) */
|
||||
export const PROVINCES = [
|
||||
'京',
|
||||
'津',
|
||||
'沪',
|
||||
'渝',
|
||||
'冀',
|
||||
'豫',
|
||||
'云',
|
||||
'辽',
|
||||
'黑',
|
||||
'湘',
|
||||
'皖',
|
||||
'鲁',
|
||||
'新',
|
||||
'苏',
|
||||
'浙',
|
||||
'赣',
|
||||
'鄂',
|
||||
'桂',
|
||||
'甘',
|
||||
'晋',
|
||||
'蒙',
|
||||
'陕',
|
||||
'吉',
|
||||
'闽',
|
||||
'贵',
|
||||
'粤',
|
||||
'青',
|
||||
'藏',
|
||||
'川',
|
||||
'宁',
|
||||
'琼'
|
||||
] as const;
|
||||
|
||||
/** 省份正则字符集 */
|
||||
const PROVINCE_CHARS = `[${PROVINCES.join('')}]`;
|
||||
|
||||
/** 合法字母数字(排除 I、O) */
|
||||
const ALPHANUM = `[A-HJ-NP-Z0-9]`;
|
||||
|
||||
/** 纯数字 */
|
||||
const DIGIT = `[0-9]`;
|
||||
|
||||
/** 字母(排除 I、O) */
|
||||
const LETTER = `[A-HJ-NP-Z]`;
|
||||
|
||||
/** 车牌类型枚举 */
|
||||
export enum PlateType {
|
||||
/** 普通蓝牌/黄牌(7位) */
|
||||
STANDARD = 'standard',
|
||||
/** 新能源小车(8位) */
|
||||
NEW_ENERGY_SMALL = 'newEnergySmall',
|
||||
/** 新能源大车(8位) */
|
||||
NEW_ENERGY_LARGE = 'newEnergyLarge',
|
||||
/** 警车(7位) */
|
||||
POLICE = 'police',
|
||||
/** 教练车(7位) */
|
||||
COACH = 'coach',
|
||||
/** 挂车(7位) */
|
||||
TRAILER = 'trailer',
|
||||
/** 港澳入出境车(7位) */
|
||||
HK_MACAU = 'hkMacau',
|
||||
/** 临时车牌(8位) */
|
||||
TEMPORARY = 'temporary',
|
||||
/** 使馆车 */
|
||||
EMBASSY = 'embassy',
|
||||
/** 领事馆车(7位) */
|
||||
CONSULATE = 'consulate',
|
||||
/** 武警车 */
|
||||
ARMED_POLICE = 'armedPolice',
|
||||
/** 试车牌(7位) */
|
||||
TEST = 'test'
|
||||
}
|
||||
|
||||
/** 车牌类型中文标签 */
|
||||
export const PLATE_TYPE_LABELS: Record<string, string> = {
|
||||
[PlateType.STANDARD]: '普通车牌',
|
||||
[PlateType.NEW_ENERGY_SMALL]: '新能源小型车',
|
||||
[PlateType.NEW_ENERGY_LARGE]: '新能源大型车',
|
||||
[PlateType.POLICE]: '警车',
|
||||
[PlateType.COACH]: '教练车',
|
||||
[PlateType.TRAILER]: '挂车',
|
||||
[PlateType.HK_MACAU]: '港澳入出境车',
|
||||
[PlateType.TEMPORARY]: '临时车牌',
|
||||
[PlateType.EMBASSY]: '使馆车',
|
||||
[PlateType.CONSULATE]: '领事馆车',
|
||||
[PlateType.ARMED_POLICE]: '武警车',
|
||||
[PlateType.TEST]: '试车牌'
|
||||
};
|
||||
|
||||
/** 车牌类型颜色(用于 UI 标识) */
|
||||
export const PLATE_TYPE_COLORS: Record<string, string> = {
|
||||
[PlateType.STANDARD]: '#409eff', // 蓝色
|
||||
[PlateType.NEW_ENERGY_SMALL]: '#67c23a', // 绿色
|
||||
[PlateType.NEW_ENERGY_LARGE]: '#67c23a', // 绿色
|
||||
[PlateType.POLICE]: '#f56c6c', // 红色
|
||||
[PlateType.COACH]: '#e6a23c', // 黄色
|
||||
[PlateType.TRAILER]: '#e6a23c', // 黄色
|
||||
[PlateType.HK_MACAU]: '#909399', // 灰色
|
||||
[PlateType.TEMPORARY]: '#9b59b6', // 紫色
|
||||
[PlateType.EMBASSY]: '#909399', // 灰色
|
||||
[PlateType.CONSULATE]: '#909399', // 灰色
|
||||
[PlateType.ARMED_POLICE]: '#f56c6c', // 红色
|
||||
[PlateType.TEST]: '#909399' // 灰色
|
||||
};
|
||||
|
||||
/** 特殊结尾字符 */
|
||||
export const SPECIAL_CHARS = ['警', '学', '挂', '临', '港', '澳', '领', '试'] as const;
|
||||
|
||||
/** 首位可输入字符集说明 */
|
||||
export const POSITION_RULES: { position: number; description: string }[] = [
|
||||
{ position: 0, description: '省份简称(京、津、沪、渝等31个)' },
|
||||
{ position: 1, description: '发牌机关代号字母(A-Z,排除I、O)' },
|
||||
{ position: 2, description: '字母或数字(新能源小车此位为D纯电/F非纯电)' },
|
||||
{ position: 3, description: '字母或数字' },
|
||||
{ position: 4, description: '字母或数字' },
|
||||
{ position: 5, description: '字母或数字' },
|
||||
{ position: 6, description: '字母或数字,或特殊字符(警/学/挂/港/澳/领/试)' },
|
||||
{ position: 7, description: '字母或数字,或特殊字符(临)/新能源标识(D/F)' }
|
||||
];
|
||||
|
||||
// ===================== 正则表达式 =====================
|
||||
|
||||
/**
|
||||
* 各类车牌正则表达式
|
||||
*
|
||||
* 基础格式说明:
|
||||
* - 省份: 1位(31个省份简称)
|
||||
* - 字母: 发牌机关代号(排除I、O避免与数字1、0混淆)
|
||||
* - 中间位: 字母或数字组合
|
||||
* - 特殊结尾: 警/学/挂/港/澳/领/临/试
|
||||
* - 新能源标识: D(纯电)/ F(非纯电/插混/燃料电池)
|
||||
*/
|
||||
const PATTERNS: Record<PlateType, RegExp> = {
|
||||
/**
|
||||
* 普通蓝牌/黄牌(7位)
|
||||
* 格式: 省 + 字母 + 5位字母数字
|
||||
* 示例: 京A12345, 京A123AB, 京AB1234
|
||||
* 适用: 小型汽车(蓝牌)、大型汽车(黄牌)、普通摩托车(黄牌)、轻便摩托车(蓝牌)
|
||||
*/
|
||||
[PlateType.STANDARD]: new RegExp(`^${PROVINCE_CHARS}${LETTER}${ALPHANUM}{5}$`),
|
||||
|
||||
/**
|
||||
* 新能源小型车(8位)
|
||||
* 格式: 省 + 字母 + D/F + 5位字母数字
|
||||
* 示例: 京AD12345, 沪AF12345
|
||||
* 说明: D=纯电动, F=非纯电动(插电混动/增程式/燃料电池)
|
||||
*/
|
||||
[PlateType.NEW_ENERGY_SMALL]: new RegExp(`^${PROVINCE_CHARS}${LETTER}[DF]${ALPHANUM}{5}$`),
|
||||
|
||||
/**
|
||||
* 新能源大型车(8位)
|
||||
* 格式: 省 + 字母 + 5位字母数字 + D/F
|
||||
* 示例: 京A12345D, 沪A12345F
|
||||
*/
|
||||
[PlateType.NEW_ENERGY_LARGE]: new RegExp(`^${PROVINCE_CHARS}${LETTER}${ALPHANUM}{5}[DF]$`),
|
||||
|
||||
/**
|
||||
* 警车牌(7位)
|
||||
* 格式: 省 + 字母 + 4位数字 + 警
|
||||
* 示例: 京A1234警, 粤B5678警
|
||||
*/
|
||||
[PlateType.POLICE]: new RegExp(`^${PROVINCE_CHARS}${LETTER}${DIGIT}{4}警$`),
|
||||
|
||||
/**
|
||||
* 教练车牌(7位)
|
||||
* 格式: 省 + 字母 + 4位字母数字 + 学
|
||||
* 示例: 京A1234学, 粤B123A学
|
||||
*/
|
||||
[PlateType.COACH]: new RegExp(`^${PROVINCE_CHARS}${LETTER}${ALPHANUM}{4}学$`),
|
||||
|
||||
/**
|
||||
* 挂车牌(7位)
|
||||
* 格式: 省 + 字母 + 4位字母数字 + 挂
|
||||
* 示例: 京A1234挂, 鲁B5678挂
|
||||
*/
|
||||
[PlateType.TRAILER]: new RegExp(`^${PROVINCE_CHARS}${LETTER}${ALPHANUM}{4}挂$`),
|
||||
|
||||
/**
|
||||
* 港澳入出境车牌(7位)
|
||||
* 格式: 省 + Z + 4位数字 + 港/澳
|
||||
* 示例: 粤Z1234港, 粤Z5678澳
|
||||
* 说明: 第二位固定为Z,黑底白字
|
||||
*/
|
||||
[PlateType.HK_MACAU]: new RegExp(`^${PROVINCE_CHARS}Z${DIGIT}{4}[港澳]$`),
|
||||
|
||||
/**
|
||||
* 临时行驶车牌(8位)
|
||||
* 格式: 省 + 字母 + 5位字母数字 + 临
|
||||
* 示例: 京A12345临, 沪B67890临
|
||||
*/
|
||||
[PlateType.TEMPORARY]: new RegExp(`^${PROVINCE_CHARS}${LETTER}${ALPHANUM}{5}临$`),
|
||||
|
||||
/**
|
||||
* 使馆车牌
|
||||
* 格式: 使 + 3~5位数字
|
||||
* 示例: 使123, 使12345
|
||||
* 说明: 黑底白字,不以省份开头
|
||||
*/
|
||||
[PlateType.EMBASSY]: /^使[0-9]{3,5}$/,
|
||||
|
||||
/**
|
||||
* 领事馆车牌(7位)
|
||||
* 格式: 省 + 字母 + 4位数字 + 领
|
||||
* 示例: 京A1234领, 沪B5678领
|
||||
*/
|
||||
[PlateType.CONSULATE]: new RegExp(`^${PROVINCE_CHARS}${LETTER}${DIGIT}{4}领$`),
|
||||
|
||||
/**
|
||||
* 武警车牌
|
||||
* 格式1: WJ + 省 + 4位数字 + 字母 (例: WJ京1234X)
|
||||
* 格式2: WJ + 5位数字 + 字母 (例: WJ12345X)
|
||||
* 格式3: WJ + 省 + 5位数字 (例: WJ京12345)
|
||||
* 说明: 白底红字"WJ"
|
||||
*/
|
||||
[PlateType.ARMED_POLICE]: /^WJ(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼])?[0-9]{4,5}[A-Z]?$/,
|
||||
|
||||
/**
|
||||
* 试车牌(7位)
|
||||
* 格式: 省 + 字母 + 试 + 4位字母数字
|
||||
* 示例: 京A试1234, 沪B试5678
|
||||
*/
|
||||
[PlateType.TEST]: new RegExp(`^${PROVINCE_CHARS}${LETTER}试${ALPHANUM}{4}$`)
|
||||
};
|
||||
|
||||
// ===================== 合理性校验 =====================
|
||||
|
||||
/**
|
||||
* 字母数字混合段至少包含 N 位数字的规则。
|
||||
*
|
||||
* 车牌序号(字母数字混合)不能全为字母——
|
||||
* 现实中不存在 京AAAAAA / 京ADAAAAA / 京A试AAAA 这类车牌。
|
||||
*/
|
||||
interface DigitRequirement {
|
||||
/** 需要统计数字的起始索引(含) */
|
||||
start: number;
|
||||
/** 需要统计数字的结束索引(含) */
|
||||
end: number;
|
||||
/** 区间内最少数字个数 */
|
||||
minDigits: number;
|
||||
}
|
||||
|
||||
/** 各类型字母数字段的数字最低要求 */
|
||||
const DIGIT_REQUIREMENT: Partial<Record<PlateType, DigitRequirement>> = {
|
||||
[PlateType.STANDARD]: { start: 2, end: 6, minDigits: 1 },
|
||||
[PlateType.NEW_ENERGY_SMALL]: { start: 3, end: 7, minDigits: 1 },
|
||||
[PlateType.NEW_ENERGY_LARGE]: { start: 2, end: 6, minDigits: 1 },
|
||||
[PlateType.COACH]: { start: 2, end: 5, minDigits: 1 },
|
||||
[PlateType.TRAILER]: { start: 2, end: 5, minDigits: 1 },
|
||||
[PlateType.TEMPORARY]: { start: 2, end: 6, minDigits: 1 },
|
||||
[PlateType.TEST]: { start: 3, end: 6, minDigits: 1 }
|
||||
};
|
||||
|
||||
// ===================== 校验类 =====================
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
type?: PlateType;
|
||||
label?: string;
|
||||
color?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class LicensePlateValidator2 {
|
||||
/** 省份列表 */
|
||||
static readonly PROVINCES = PROVINCES;
|
||||
|
||||
/** 特殊字符列表 */
|
||||
static readonly SPECIAL_CHARS = SPECIAL_CHARS;
|
||||
|
||||
/** 所有正则 */
|
||||
static readonly PATTERNS = PATTERNS;
|
||||
|
||||
// ---- 公开方法 ----
|
||||
|
||||
/**
|
||||
* 校验车牌号是否合法(格式 + 合理性)
|
||||
* @param value 车牌号字符串
|
||||
* @returns 是否合法
|
||||
*/
|
||||
static isValidPlate(value: string): boolean {
|
||||
const val = (value || '').toUpperCase().trim();
|
||||
if (!val) return false;
|
||||
// 统一走 getPlateType,确保格式校验和合理性校验一致
|
||||
return this.getPlateType(val) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车牌类型(含格式校验 + 合理性校验)
|
||||
* @param value 车牌号字符串
|
||||
* @returns 车牌类型枚举,未匹配返回 null
|
||||
*/
|
||||
static getPlateType(value: string): PlateType | null {
|
||||
const val = (value || '').toUpperCase().trim();
|
||||
if (!val) return null;
|
||||
|
||||
// 使馆车(不以省份开头,先匹配再校验数字段不需要额外检查)
|
||||
if (val.startsWith('使') && PATTERNS[PlateType.EMBASSY].test(val)) {
|
||||
return PlateType.EMBASSY;
|
||||
}
|
||||
|
||||
// 武警车(以WJ开头)
|
||||
if (val.startsWith('WJ') && PATTERNS[PlateType.ARMED_POLICE].test(val)) {
|
||||
return PlateType.ARMED_POLICE;
|
||||
}
|
||||
|
||||
// 以省份开头的车牌
|
||||
if (PROVINCES.includes(val[0] as any)) {
|
||||
// 按优先级逐条匹配(特殊结尾 > 新能源 > 普通)
|
||||
|
||||
if (val.endsWith('警') && PATTERNS[PlateType.POLICE].test(val)) {
|
||||
return PlateType.POLICE;
|
||||
}
|
||||
if (val.endsWith('学')) {
|
||||
if (PATTERNS[PlateType.COACH].test(val) && this._checkDigits(val, PlateType.COACH)) {
|
||||
return PlateType.COACH;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (val.endsWith('挂')) {
|
||||
if (PATTERNS[PlateType.TRAILER].test(val) && this._checkDigits(val, PlateType.TRAILER)) {
|
||||
return PlateType.TRAILER;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (val.endsWith('港') || val.endsWith('澳')) {
|
||||
if (PATTERNS[PlateType.HK_MACAU].test(val)) {
|
||||
return PlateType.HK_MACAU;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (val.endsWith('领') && PATTERNS[PlateType.CONSULATE].test(val)) {
|
||||
return PlateType.CONSULATE;
|
||||
}
|
||||
if (val.endsWith('临')) {
|
||||
if (PATTERNS[PlateType.TEMPORARY].test(val) && this._checkDigits(val, PlateType.TEMPORARY)) {
|
||||
return PlateType.TEMPORARY;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (val.includes('试')) {
|
||||
if (PATTERNS[PlateType.TEST].test(val) && this._checkDigits(val, PlateType.TEST)) {
|
||||
return PlateType.TEST;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 8位新能源
|
||||
if (val.length === 8) {
|
||||
// 新能源小车: 第3位是D或F
|
||||
if (/^[DF]$/.test(val[2]) && PATTERNS[PlateType.NEW_ENERGY_SMALL].test(val) && this._checkDigits(val, PlateType.NEW_ENERGY_SMALL)) {
|
||||
return PlateType.NEW_ENERGY_SMALL;
|
||||
}
|
||||
// 新能源大车: 最后一位是D或F
|
||||
if (/^[DF]$/.test(val[7]) && PATTERNS[PlateType.NEW_ENERGY_LARGE].test(val) && this._checkDigits(val, PlateType.NEW_ENERGY_LARGE)) {
|
||||
return PlateType.NEW_ENERGY_LARGE;
|
||||
}
|
||||
}
|
||||
|
||||
// 普通蓝牌/黄牌(7位)
|
||||
if (val.length === 7 && PATTERNS[PlateType.STANDARD].test(val) && this._checkDigits(val, PlateType.STANDARD)) {
|
||||
return PlateType.STANDARD;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整校验(返回类型和描述)
|
||||
* @param value 车牌号字符串
|
||||
* @returns 校验结果对象
|
||||
*/
|
||||
static validate(value: string): ValidationResult {
|
||||
const val = (value || '').toUpperCase().trim();
|
||||
|
||||
if (!val) {
|
||||
return { valid: false, message: '请输入车牌号' };
|
||||
}
|
||||
|
||||
// 格式预检(提供更精准的错误信息)
|
||||
const formatError = this._formatCheck(val);
|
||||
if (formatError) {
|
||||
return formatError;
|
||||
}
|
||||
|
||||
// 合法性校验
|
||||
const type = this.getPlateType(val);
|
||||
|
||||
if (!type) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
type,
|
||||
label: PLATE_TYPE_LABELS[type],
|
||||
color: PLATE_TYPE_COLORS[type]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取车牌类型描述信息
|
||||
* @param value 车牌号字符串
|
||||
* @returns 描述字符串
|
||||
*/
|
||||
static getPlateInfo(value: string): string {
|
||||
const result = this.validate(value);
|
||||
if (result.valid && result.label) {
|
||||
return result.label;
|
||||
}
|
||||
return result.message || '未知格式';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的车牌类型及其格式说明
|
||||
*/
|
||||
static getAllPlateTypes(): { type: PlateType; label: string; format: string; example: string; color: string; length: number }[] {
|
||||
return [
|
||||
{
|
||||
type: PlateType.STANDARD,
|
||||
label: '普通蓝牌/黄牌',
|
||||
format: '省+字母+5位字母数字',
|
||||
example: '京A12345',
|
||||
color: PLATE_TYPE_COLORS[PlateType.STANDARD],
|
||||
length: 7
|
||||
},
|
||||
{
|
||||
type: PlateType.NEW_ENERGY_SMALL,
|
||||
label: '新能源小型车(纯电D/非纯电F)',
|
||||
format: '省+字母+D/F+5位字母数字',
|
||||
example: '京AD12345',
|
||||
color: PLATE_TYPE_COLORS[PlateType.NEW_ENERGY_SMALL],
|
||||
length: 8
|
||||
},
|
||||
{
|
||||
type: PlateType.NEW_ENERGY_LARGE,
|
||||
label: '新能源大型车(纯电D/非纯电F)',
|
||||
format: '省+字母+5位字母数字+D/F',
|
||||
example: '京A12345D',
|
||||
color: PLATE_TYPE_COLORS[PlateType.NEW_ENERGY_LARGE],
|
||||
length: 8
|
||||
},
|
||||
{
|
||||
type: PlateType.POLICE,
|
||||
label: '警车',
|
||||
format: '省+字母+4位数字+警',
|
||||
example: '京A1234警',
|
||||
color: PLATE_TYPE_COLORS[PlateType.POLICE],
|
||||
length: 7
|
||||
},
|
||||
{
|
||||
type: PlateType.COACH,
|
||||
label: '教练车',
|
||||
format: '省+字母+4位字母数字+学',
|
||||
example: '京A1234学',
|
||||
color: PLATE_TYPE_COLORS[PlateType.COACH],
|
||||
length: 7
|
||||
},
|
||||
{
|
||||
type: PlateType.TRAILER,
|
||||
label: '挂车',
|
||||
format: '省+字母+4位字母数字+挂',
|
||||
example: '京A1234挂',
|
||||
color: PLATE_TYPE_COLORS[PlateType.TRAILER],
|
||||
length: 7
|
||||
},
|
||||
{
|
||||
type: PlateType.HK_MACAU,
|
||||
label: '港澳入出境车',
|
||||
format: '省+Z+4位数字+港/澳',
|
||||
example: '粤Z1234港',
|
||||
color: PLATE_TYPE_COLORS[PlateType.HK_MACAU],
|
||||
length: 7
|
||||
},
|
||||
{
|
||||
type: PlateType.TEMPORARY,
|
||||
label: '临时车牌',
|
||||
format: '省+字母+5位字母数字+临',
|
||||
example: '京A12345临',
|
||||
color: PLATE_TYPE_COLORS[PlateType.TEMPORARY],
|
||||
length: 8
|
||||
},
|
||||
{
|
||||
type: PlateType.CONSULATE,
|
||||
label: '领事馆车',
|
||||
format: '省+字母+4位数字+领',
|
||||
example: '京A1234领',
|
||||
color: PLATE_TYPE_COLORS[PlateType.CONSULATE],
|
||||
length: 7
|
||||
},
|
||||
{
|
||||
type: PlateType.TEST,
|
||||
label: '试车牌',
|
||||
format: '省+字母+试+4位字母数字',
|
||||
example: '京A试1234',
|
||||
color: PLATE_TYPE_COLORS[PlateType.TEST],
|
||||
length: 7
|
||||
},
|
||||
{
|
||||
type: PlateType.EMBASSY,
|
||||
label: '使馆车',
|
||||
format: '使+3~5位数字',
|
||||
example: '使12345',
|
||||
color: PLATE_TYPE_COLORS[PlateType.EMBASSY],
|
||||
length: 0
|
||||
},
|
||||
{
|
||||
type: PlateType.ARMED_POLICE,
|
||||
label: '武警车',
|
||||
format: 'WJ+省(可选)+4~5位数字+字母(可选)',
|
||||
example: 'WJ京1234X',
|
||||
color: PLATE_TYPE_COLORS[PlateType.ARMED_POLICE],
|
||||
length: 0
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验指定位置的字符是否合法(逐字输入场景,如多格拆分输入框)
|
||||
* @param char 待校验字符
|
||||
* @param index 位置索引(0-7)
|
||||
* @param currentChars 当前所有字符(用于上下文判断)
|
||||
* @returns 是否合法
|
||||
*/
|
||||
static isValidCharAtPosition(char: string, index: number, currentChars: string[] = []): boolean {
|
||||
if (!char) return true;
|
||||
|
||||
const c = char.toUpperCase();
|
||||
const joined = currentChars.join('').toUpperCase().trim();
|
||||
|
||||
// 使馆车(以"使"开头):后续只能输入数字
|
||||
if (index === 0 && c === '使') return true;
|
||||
if (joined.startsWith('使')) {
|
||||
return /[0-9]/.test(c);
|
||||
}
|
||||
|
||||
// 武警车(WJ 前缀):前两位必须依次是 W、J
|
||||
if (index === 0 && c === 'W') return true;
|
||||
if (index === 1 && joined[0] === 'W' && c === 'J') return true;
|
||||
if (joined.startsWith('WJ')) {
|
||||
// WJ 后可以是 [省份] 或 [数字]
|
||||
if (index === 2 && PROVINCES.includes(c as any)) return true;
|
||||
// 数字段
|
||||
if (index >= 2 && /[0-9]/.test(c)) return true;
|
||||
// 末尾可选字母
|
||||
if (index >= 6 && index <= 7 && /[A-Z]/.test(c)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 位置 0:必须是省份(W 已被 WJ 分支处理,不会到此处)
|
||||
if (index === 0) {
|
||||
return PROVINCES.includes(c as any);
|
||||
}
|
||||
|
||||
// 位置 1:必须是字母(排除 I、O)
|
||||
if (index === 1) {
|
||||
return /^[A-HJ-NP-Z]$/i.test(c);
|
||||
}
|
||||
|
||||
// 位置 2-5:字母或数字
|
||||
if (index >= 2 && index <= 5) {
|
||||
return /^[A-HJ-NP-Z0-9]$/i.test(c);
|
||||
}
|
||||
|
||||
// 位置 6:字母数字 或 特殊结尾(警/学/挂/港/澳/领/试),排除"临"
|
||||
if (index === 6) {
|
||||
if (SPECIAL_CHARS.includes(c as any)) {
|
||||
if (c === '临') return false;
|
||||
return true;
|
||||
}
|
||||
return /^[A-HJ-NP-Z0-9]$/i.test(c);
|
||||
}
|
||||
|
||||
// 位置 7:字母数字 或 D/F(新能源大车)或 临(临时车牌)
|
||||
if (index === 7) {
|
||||
if (c === '临') return true;
|
||||
return /^[A-HJ-NP-Z0-9]$/i.test(c);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前已输入内容推测车牌长度(7或8位)
|
||||
* @param currentChars 当前已输入字符数组
|
||||
* @returns 推测的车牌长度
|
||||
*/
|
||||
static guessPlateLength(currentChars: string[]): 7 | 8 {
|
||||
const joined = currentChars.join('').toUpperCase().trim();
|
||||
|
||||
// 已有确定长度的特殊结尾
|
||||
if (['警', '学', '挂', '领', '港', '澳'].some((s) => joined.endsWith(s))) return 7;
|
||||
if (joined.endsWith('临')) return 8;
|
||||
if (joined.includes('试')) return 7;
|
||||
|
||||
// 第3位是D或F → 新能源小车(8位)
|
||||
if (joined.length >= 3 && /^[DF]$/.test(joined[2])) return 8;
|
||||
|
||||
// 默认7位
|
||||
if (joined.length <= 7) return 7;
|
||||
|
||||
// 已满8位,检查最后一位是否为D/F
|
||||
if (joined.length === 8 && /^[DF]$/.test(joined[7])) return 8;
|
||||
|
||||
return 8;
|
||||
}
|
||||
|
||||
// ---- 内部方法 ----
|
||||
|
||||
/**
|
||||
* 合理性校验:字母数字混合段至少包含 minDigits 位数字
|
||||
* 用于拒绝京AAAAAA / 京ADAAAAA / 京A试AAAA 等纯字母组合
|
||||
*/
|
||||
private static _checkDigits(val: string, type: PlateType): boolean {
|
||||
const req = DIGIT_REQUIREMENT[type];
|
||||
if (!req) return true; // 无数字要求则通过
|
||||
|
||||
let count = 0;
|
||||
const end = Math.min(req.end, val.length - 1);
|
||||
for (let i = req.start; i <= end; i++) {
|
||||
if (/[0-9]/.test(val[i])) count++;
|
||||
}
|
||||
return count >= req.minDigits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式预检:在 getPlateType 之前做轻量格式判断,给出更友好的错误提示
|
||||
*/
|
||||
private static _formatCheck(val: string): ValidationResult | null {
|
||||
// 首位检查(使馆和武警由 getPlateType 处理)
|
||||
if (!PROVINCES.includes(val[0] as any) && !val.startsWith('使') && !val.startsWith('WJ')) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
|
||||
// 长度检查
|
||||
if (val.startsWith('使')) {
|
||||
if (val.length < 4) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
if (val.length > 6) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (val.startsWith('WJ')) {
|
||||
if (val.length < 6) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
if (val.length > 9) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 省份开头的车牌
|
||||
if (val.length < 7) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
if (val.length > 8) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
|
||||
// 第二位检查(排除 I、O)
|
||||
if (val.length >= 2 && /[IO]/.test(val[1])) {
|
||||
return { valid: false, message: '请输入正确的车牌号' };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default LicensePlateValidator2;
|
||||
@@ -134,8 +134,8 @@ export function add(a: string | number, b: string | number): number {
|
||||
/**
|
||||
* 乘法 a * b
|
||||
*/
|
||||
export function mul(a: string | number, b: string | number): number {
|
||||
return Number(toBig(a).times(toBig(b)));
|
||||
export function mul(a: string | number, b: string | number, decimal = 2): number {
|
||||
return Number(toBig(a).times(toBig(b)).round(decimal));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,6 +147,50 @@ export function div(a: string | number, b: string | number): number {
|
||||
if (bigB.eq(0)) throw new Error('除数不能为0');
|
||||
return Number(toBig(a).div(bigB));
|
||||
}
|
||||
|
||||
/**
|
||||
* a > b
|
||||
*/
|
||||
export function gtMoney(a: string | number, b: string | number): boolean {
|
||||
return toBig(a).gt(toBig(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* a >= b
|
||||
*/
|
||||
export function gteMoney(a: string | number, b: string | number): boolean {
|
||||
return toBig(a).gte(toBig(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* a < b
|
||||
*/
|
||||
export function ltMoney(a: string | number, b: string | number): boolean {
|
||||
return toBig(a).lt(toBig(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* a <= b
|
||||
*/
|
||||
export function lteMoney(a: string | number, b: string | number): boolean {
|
||||
return toBig(a).lte(toBig(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* a === b 金额相等
|
||||
*/
|
||||
export function eqMoney(a: string | number, b: string | number): boolean {
|
||||
return toBig(a).eq(toBig(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* a !== b 金额不相等
|
||||
*/
|
||||
export function neqMoney(a: string | number, b: string | number): boolean {
|
||||
return !toBig(a).eq(toBig(b));
|
||||
}
|
||||
|
||||
|
||||
// 保留2位小数(金额专用)
|
||||
export function toFixed2(val) {
|
||||
return Number(val).toFixed(2);
|
||||
|
||||
@@ -78,7 +78,7 @@ service.interceptors.request.use(
|
||||
const villageid = localStorage.getItem('villageid');
|
||||
config.headers['Content-Language'] = getLanguage();
|
||||
config.headers['villageId'] = villageid;
|
||||
const disableAutoVillage = config.headers?.autoVillage === false;
|
||||
// const disableAutoVillage = config.headers?.autoVillage === false;
|
||||
|
||||
const isToken = config.headers?.isToken === false;
|
||||
// 是否需要防止数据重复提交
|
||||
|
||||
Reference in New Issue
Block a user