feat(common): 添加input-rules

This commit is contained in:
CodiceFabbrica 2026-03-18 15:52:07 +08:00
parent 0cecc4df11
commit 5851d40d07
7 changed files with 400 additions and 101 deletions

2
.czrc
View File

@ -1,3 +1,3 @@
{ {
"path": "node_modules/cz-git" "path": "cz-git"
} }

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"psioniq.psi-header",
"aaron-bond.better-comments",
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint"
]
}

29
.vscode/settings.json vendored
View File

@ -1,3 +1,30 @@
{ {
"psi-header.config": {
"forceToTop": true,
"blankLinesAfter": 1,
"overridePathSeparator": "/"
},
"psi-header.changes-tracking": {
"isActive": true,
"autoHeader": "autoSave",
"modDate": "@lastModified:",
"modDateFormat": "YYYY-MM-DD HH:mm:ss",
"modAuthor": "@lastModifiedBy:",
"exclude": ["markdown", "json"],
"excludeGlob": ["**/settings.json"]
},
"psi-header.templates": [
{
"language": "*",
"template": [
"@file: <<filerelativepath>>",
"@description: ",
"@author: <<author>> (<<authoremail>>)",
"@date: <<filecreated('YYYY-MM-DD HH:mm:ss')>>",
"@lastModified: <<dateformat('YYYY-MM-DD HH:mm:ss')>>",
"@lastModifiedBy: <<author>> (<<authoremail>>)"
],
"changeLogNaturalOrder": true
}
]
} }

View File

@ -77,105 +77,39 @@ module.exports = {
allowCustomScopes: false, allowCustomScopes: false,
allowEmptyScopes: true, allowEmptyScopes: true,
messages: { messages: {
skip: "跳过", type: '选择你要提交的类型:',
max: "最多 %d 个字符", scope: '选择一个提交范围(可选):',
min: "最少 %d 个字符", customScope: '请输入自定义范围:',
emptyWarning: "不能为空", subject: '请简短描述这次改动:\n',
upperLimitWarning: "超出字数限制", body: '请详细描述这次改动(可选,用 "|" 换行):\n',
lowerLimitWarning: "低于字数限制", breaking: '是否有破坏性变更(可选,用 "|" 换行):\n',
}, footerPrefixesSelect: '选择关联的 ISSUE 类型(可选):',
questions: { customFooterPrefix: '输入 ISSUE 前缀:',
type: { footer: '列出关联的 ISSUE如: #31, #34:\n',
description: "选择提交类型", generatingByAI: '正在通过 AI 生成提交信息...',
enum: { generatedSelectByAI: '选择一个 AI 生成的 subject:',
feat: { confirmCommit: '是否确认提交以上信息?',
description: "一个新特性",
title: "新特性",
emoji: "✨",
},
fix: {
description: "一个漏洞修复",
title: "漏洞修复",
emoji: "🐛",
},
docs: {
description: "仅更改文档",
title: "文档",
emoji: "📚",
},
style: {
description: "不影响代码含义的更改(空白符、格式化、缺少分号等)",
title: "样式",
emoji: "💎",
},
refactor: {
description: "既不修复错误也不添加功能的代码更改",
title: "重构",
emoji: "📦",
},
perf: {
description: "提高性能的代码更改",
title: "性能提升",
emoji: "🚀",
},
test: {
description: "添加缺失的测试或纠正现有的测试",
title: "测试",
emoji: "🚨",
},
build: {
description:
"影响构建系统或外部依赖项的更改例如gulp、broccoli、npm",
title: "构建",
emoji: "🛠",
},
ci: {
description:
"更改我们的集成配置文件和脚本例如Travis、Circle、BrowserStack、SauceLabs",
title: "集成",
emoji: "⚙️",
},
chore: {
description: "不修改源码或测试文件的其他更改",
title: "杂项",
emoji: "♻️",
},
revert: {
description: "恢复之前的提交",
title: "还原",
emoji: "🗑",
},
},
},
scope: {
description: "更改的范围scopecommon、vue3、uni-app",
},
subject: {
description: "主题subject",
},
body: {
description: "主体body",
},
isBreaking: {
description: "是否有破坏性更改BREAKING CHANGE",
},
breakingBody: {
description:
"破坏性更改BREAKING CHANGE需要一个主体body请输入详细描述",
},
breaking: {
description: "描述破坏性更改BREAKING CHANGE",
},
isIssueAffected: {
description: "此改变是否影响打开的问题open ssues",
},
issuesBody: {
description: "如果问题关闭则需要提交一个主体body请输入详细描述",
},
issues: {
description:
'添加问题参考issue references (例: "fix #123"、"re #123"',
},
}, },
// subject 最小长度提示(中文化 "[x more chars needed]"
minSubjectLength: 4,
defaultSubject: '',
types: [
{ value: 'feat', name: 'feat: 新增功能', emoji: ':sparkles:' },
{ value: 'fix', name: 'fix: 修复缺陷', emoji: ':bug:' },
{ value: 'docs', name: 'docs: 文档变更', emoji: ':memo:' },
{ value: 'style', name: 'style: 代码格式(不影响逻辑)', emoji: ':lipstick:' },
{ value: 'refactor', name: 'refactor: 重构(既非新功能,也不是修 bug', emoji: ':recycle:' },
{ value: 'perf', name: 'perf: 性能优化', emoji: ':zap:' },
{ value: 'test', name: 'test: 添加或修改测试', emoji: ':white_check_mark:' },
{ value: 'build', name: 'build: 构建系统或外部依赖变更', emoji: ':package:' },
{ value: 'ci', name: 'ci: CI 配置文件和脚本变更', emoji: ':ferris_wheel:' },
{ value: 'chore', name: 'chore: 其它不修改 src 或测试的改动', emoji: ':hammer:' },
{ value: 'revert', name: 'revert: 回退之前的 commit', emoji: ':rewind:' },
],
// issue 前缀也可以用中文说明
issuePrefixs: [
{ value: 'link', name: 'link: 关联 ISSUE' },
{ value: 'close', name: 'close: 关闭 ISSUE' },
],
}, },
}; };

View File

@ -3,3 +3,4 @@ export * from "./permission";
export * from "./printer"; export * from "./printer";
export * from "./time"; export * from "./time";
export * from "./timer"; export * from "./timer";
export * from "./input-rule";

View File

@ -0,0 +1,163 @@
/*
* @file: /src/input-rule.ts
* @description:
* @author: tsl (randy1924@163.com)
* @date: 2026-03-18 13:37:11
* @lastModified: 2026-03-18 15:04:44
* @lastModifiedBy: tsl (randy1924@163.com)
*/
export type InputValue = string | number | null;
export interface InputOptions {
min?: number; // 最小值
max?: number; // 最大值
init?: string | number; // 初始值
digits?: number; // 小数位数
pattern?: string | RegExp; // 匹配模式
required?: boolean; // 是否必填
integer?: boolean; // 是否整数
number?: boolean; // 是否转成数字
text?: boolean; // 是否使用文本模式
floor?: boolean; // 是否向下取数
ceil?: boolean; // 是否向上取数
keepDecimal?: boolean; // 是否保留小数即使是小数位是0
noSign?: boolean; // 不允许输入正负号
}
/**
*
* @example
* 099999.992
* const value = getValueOfRule(e.detail.value, {
* min: 0,
* max: 99999.99,
* required: true,
* digits: 2,
* })
*/
export function getValueOfRule(
value: InputValue,
options: InputOptions,
callback?: (value: InputValue) => void,
): InputValue {
console.log("getValueOfRule", value, options);
const { text } = options;
let targetValue: InputValue;
if (text) {
targetValue = getTextTargetValue(value, options);
} else {
targetValue = getNumberTargetValue(value, options);
}
console.log("targetValue", targetValue);
callback?.(targetValue);
return targetValue;
}
/**
* -
*/
function getTextTargetValue(innerValue: InputValue, options: InputOptions): InputValue {
const { required, init, pattern, number } = options;
innerValue ??= "";
innerValue = String(innerValue);
let targetValue: string | number = innerValue;
// 如果提供了pattern参数检查输入是否匹配该模式
if (pattern && !new RegExp(pattern).test(innerValue)) {
return "";
}
if (!/^[+-]?\d+(\.\d+)?$/.test(innerValue)) {
if (required) {
if (init !== undefined) {
targetValue = String(init);
}
}
}
if (number) {
targetValue = Number(targetValue);
}
return targetValue;
}
/**
* floor/ceil
*/
function getRoundFn(floor?: boolean, ceil?: boolean): (n: number) => number {
if (floor) return Math.floor;
if (ceil) return Math.ceil;
return Math.round;
}
/**
*
*/
function isValidNumberInput(value: string, noSign?: boolean): boolean {
if (!value) return false;
if (!/^[+-]?\d+(\.\d+)?$/.test(value)) return false;
if (noSign && /[+-]/.test(value)) return false;
return true;
}
/**
* 退
*/
function getFallbackValue(options: InputOptions): string {
const { required, init, min } = options;
if (!required) return "";
if (init != null) return String(init);
return min == null ? "0" : String(min);
}
/**
*
*/
function applyPrecision(num: number, options: InputOptions): number {
const { digits, integer, floor, ceil, min, max } = options;
let result = num;
if (min != null) result = Math.max(result, min);
if (max != null) result = Math.min(result, max);
const roundFn = getRoundFn(floor, ceil);
if (digits != null) {
const precision = Math.pow(10, digits);
result = roundFn(result * precision) / precision;
}
if (integer) {
result = roundFn(result);
}
return result;
}
/**
* -
*/
function getNumberTargetValue(innerValue: InputValue, options: InputOptions): InputValue {
const { number, keepDecimal, noSign, digits } = options;
const strValue = String(innerValue ?? "");
let targetValue: string;
if (isValidNumberInput(strValue, noSign)) {
targetValue = String(applyPrecision(Number(strValue), options));
} else {
targetValue = getFallbackValue(options);
}
if (digits != null && keepDecimal) {
targetValue = Number(targetValue).toFixed(digits);
}
return number ? Number(targetValue) : targetValue;
}

View File

@ -0,0 +1,166 @@
import { test, expect, describe } from "@jest/globals";
import { getValueOfRule, InputOptions } from "../src/input-rule";
describe("getValueOfRule - 文本模式 (text: true)", () => {
const textOpts: InputOptions = { text: true };
test("普通文本直接返回", () => {
expect(getValueOfRule("hello", { ...textOpts })).toBe("hello");
});
test("null 值返回空字符串", () => {
expect(getValueOfRule(null, { ...textOpts })).toBe("");
});
test("required + init非数字文本回退到 init", () => {
expect(getValueOfRule("abc", { ...textOpts, required: true, init: "默认" })).toBe("默认");
});
test("required 无 init非数字文本保持原值", () => {
expect(getValueOfRule("abc", { ...textOpts, required: true })).toBe("abc");
});
test("number 选项将结果转为数字", () => {
expect(getValueOfRule("123", { ...textOpts, number: true })).toBe(123);
});
test("pattern 匹配失败返回空字符串", () => {
expect(getValueOfRule("abc", { ...textOpts, pattern: "^\\d+$" })).toBe("");
});
test("pattern 匹配成功返回原值", () => {
expect(getValueOfRule("123", { ...textOpts, pattern: "^\\d+$" })).toBe("123");
});
});
describe("getValueOfRule - 数字模式", () => {
test("正常数字字符串返回字符串", () => {
expect(getValueOfRule("42", {})).toBe("42");
});
test("number 选项返回数字类型", () => {
expect(getValueOfRule("42", { number: true })).toBe(42);
});
test("非法输入 + required 回退到 init", () => {
expect(getValueOfRule("abc", { required: true, init: 10 })).toBe("10");
});
test("非法输入 + required 无 init 回退到 min", () => {
expect(getValueOfRule("abc", { required: true, min: 5 })).toBe("5");
});
test("非法输入 + required 无 init 无 min 回退到 0", () => {
expect(getValueOfRule("abc", { required: true })).toBe("0");
});
test("非法输入 + 非 required 返回空字符串", () => {
expect(getValueOfRule("abc", {})).toBe("");
});
test("null 值 + required 回退到 init", () => {
expect(getValueOfRule(null, { required: true, init: 0 })).toBe("0");
});
test("null 值 + 非 required 返回空字符串", () => {
expect(getValueOfRule(null, {})).toBe("");
});
});
describe("getValueOfRule - min/max 限制", () => {
test("值小于 min 时限制为 min", () => {
expect(getValueOfRule("3", { min: 5 })).toBe("5");
});
test("值大于 max 时限制为 max", () => {
expect(getValueOfRule("100", { max: 50 })).toBe("50");
});
test("值在 min/max 范围内保持不变", () => {
expect(getValueOfRule("25", { min: 0, max: 50 })).toBe("25");
});
test("负数受 min 限制", () => {
expect(getValueOfRule("-10", { min: 0 })).toBe("0");
});
});
describe("getValueOfRule - digits 小数位数", () => {
test("保留 2 位小数(四舍五入)", () => {
expect(getValueOfRule("3.456", { digits: 2 })).toBe("3.46");
});
test("保留 0 位小数", () => {
expect(getValueOfRule("3.6", { digits: 0 })).toBe("4");
});
test("floor 向下取整小数", () => {
expect(getValueOfRule("3.456", { digits: 2, floor: true })).toBe("3.45");
});
test("ceil 向上取整小数", () => {
expect(getValueOfRule("3.451", { digits: 2, ceil: true })).toBe("3.46");
});
test("keepDecimal 保留尾部零", () => {
expect(getValueOfRule("3", { digits: 2, keepDecimal: true })).toBe("3.00");
});
test("keepDecimal + number 返回数字类型", () => {
expect(getValueOfRule("3.10", { digits: 2, keepDecimal: true, number: true })).toBe(3.1);
});
});
describe("getValueOfRule - integer 整数模式", () => {
test("integer 四舍五入取整", () => {
expect(getValueOfRule("3.6", { integer: true })).toBe("4");
});
test("integer + floor 向下取整", () => {
expect(getValueOfRule("3.9", { integer: true, floor: true })).toBe("3");
});
test("integer + ceil 向上取整", () => {
expect(getValueOfRule("3.1", { integer: true, ceil: true })).toBe("4");
});
});
describe("getValueOfRule - noSign 不允许正负号", () => {
test("带正号的输入被视为非法", () => {
expect(getValueOfRule("+5", { noSign: true, required: true, init: 0 })).toBe("0");
});
test("带负号的输入被视为非法", () => {
expect(getValueOfRule("-5", { noSign: true, required: true, init: 0 })).toBe("0");
});
test("无符号数字正常通过", () => {
expect(getValueOfRule("5", { noSign: true })).toBe("5");
});
});
describe("getValueOfRule - callback", () => {
test("callback 接收最终处理后的值", () => {
let received: unknown;
getValueOfRule("42", { number: true }, (v) => { received = v; });
expect(received).toBe(42);
});
});
describe("getValueOfRule - 综合场景", () => {
test("限制输入 min:0, max:99999.99, required, digits:2", () => {
expect(getValueOfRule("100.456", { min: 0, max: 99999.99, required: true, digits: 2 })).toBe("100.46");
});
test("超出 max 时截断到 max", () => {
expect(getValueOfRule("100000", { min: 0, max: 99999.99, required: true, digits: 2 })).toBe("99999.99");
});
test("空输入 required 回退到 min", () => {
expect(getValueOfRule("", { min: 0, max: 99999.99, required: true, digits: 2 })).toBe("0");
});
test("负数输入限制到 min:0", () => {
expect(getValueOfRule("-5", { min: 0, max: 100, required: true })).toBe("0");
});
});