feat: 构建系统重构 + VCHSM 架构迁移 + 右键菜单修复 + 尺寸参数统一

## 构建、打包与分发系统重构 (build/)

- 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本

- electron-builder 配置从 package.json 提取为 build/electron-builder.js

- 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数

- OSS 路径按渠道隔离

- 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验

- 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成

- updater.ts 发送 channel/edition/deviceFingerprint

- 新增 src/shared/utils/rollout.ts 灰度测试工具

- 新增 10 条 NPM 脚本

## VCHSM 五层架构迁移

- 7 个业务模块 + ~500 处导入路径同步

## 修复

- MediaCardContextMenu 改用共享 ContextMenu 组件

- ComboBox 尺寸参数显示统一 (size-format.ts)

## 文档

- UpdateA.md / build/README.md / README.md / .env.example 更新

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-29 18:41:07 +08:00
parent e0d91335c7
commit 337d6c1205
187 changed files with 5252 additions and 1702 deletions

View File

@@ -0,0 +1,162 @@
// ============================================================
// 校验函数 — 纯函数,与 UI 层解耦
// 每个校验器返回 string错误信息或 null通过
// ============================================================
type ValidateResult = string | null;
/** 用户名3~64 位,仅允许字母、数字、-、_、. */
export function validateUsername(value: string): ValidateResult {
if (!value || !value.trim()) return '请输入用户名';
if (value.length < 3) return '用户名至少 3 位';
if (value.length > 64) return '用户名最多 64 位';
if (!/^[a-zA-Z0-9\-_.]+$/.test(value)) return '仅允许字母、数字、-、_、.';
return null;
}
/** 密码6~128 位 */
export function validatePassword(value: string): ValidateResult {
if (!value) return '请输入密码';
if (value.length < 6) return '密码至少 6 位';
if (value.length > 128) return '密码最多 128 位';
return null;
}
/** 确认密码:与密码一致 */
export function validateConfirmPassword(value: string, password: string): ValidateResult {
if (!value) return '请再次输入密码';
if (value !== password) return '两次输入的密码不一致';
return null;
}
/** 手机号5~32 位数字(可含 + 前缀) */
export function validatePhone(value: string): ValidateResult {
if (!value) return '请输入手机号';
if (value.length < 5) return '手机号至少 5 位';
if (value.length > 32) return '手机号最多 32 位';
if (!/^\+?[0-9]+$/.test(value)) return '手机号格式不正确';
return null;
}
/** 短信验证码4~8 位数字 */
export function validateSmsCode(value: string): ValidateResult {
if (!value) return '请输入验证码';
if (value.length < 4) return '验证码至少 4 位';
if (value.length > 8) return '验证码最多 8 位';
if (!/^[0-9]+$/.test(value)) return '验证码仅允许数字';
return null;
}
/** 设备 ID8~128 位 */
export function validateDeviceId(value: string): ValidateResult {
if (!value) return '请输入设备标识';
if (value.length < 8) return '设备标识至少 8 位';
if (value.length > 128) return '设备标识最多 128 位';
return null;
}
/** 可选字段:非空时不超过 maxLen */
export function optionalMaxLen(maxLen: number, label: string) {
return (value: string): ValidateResult => {
if (!value) return null;
if (value.length > maxLen) return `${label}最多 ${maxLen}`;
return null;
};
}
/** 邀请码:可选,最多 32 位字母数字 */
export function validateReferralCode(value: string): ValidateResult {
if (!value) return null;
if (value.length > 32) return '邀请码最多 32 位';
if (!/^[a-zA-Z0-9]+$/.test(value)) return '仅允许字母和数字';
return null;
}
/**
* 对一条字段配置执行所有规则校验
* @returns 错误信息数组(空数组表示通过)
*/
export function validateField(value: string, rules: import('../model/types').FieldRule[]): string[] {
const errors: string[] = [];
for (const rule of rules) {
// required
if (rule.required && (!value || !value.trim())) {
errors.push(rule.message || '此字段为必填');
continue;
}
if (!value) continue; // 非必填且为空 → 跳过后续校验
// minLen
if (rule.minLen && value.length < rule.minLen) {
errors.push(rule.message || `至少 ${rule.minLen}`);
}
// maxLen
if (rule.maxLen && value.length > rule.maxLen) {
errors.push(rule.message || `最多 ${rule.maxLen}`);
}
// pattern
if (rule.pattern && !rule.pattern.test(value)) {
errors.push(rule.message || '格式不正确');
}
}
return errors;
}
/**
* 逐字段运行校验器,有错误则设置并返回 true。
*
* 用法(在 handleSubmit 中):
* if (runFieldChecks(setErrors,
* { field: 'phone', error: validatePhone(phone) },
* { field: 'pwd', error: validatePassword(pwd) },
* { field: 'confirm', error: validateConfirmPassword(confirm, pwd) },
* )) return;
*/
export function runFieldChecks(
setErrors: (errors: Record<string, string>) => void,
...checks: Array<{ field: string; error: string | null }>
): boolean {
const newErrors: Record<string, string> = {};
for (const { field, error } of checks) {
if (error) newErrors[field] = error;
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return true;
}
setErrors({});
return false;
}
/**
* 校验整个表单
* @returns 错误映射 { fieldName: errorMsg[] },空对象表示全部通过
*/
export function validateForm<T extends Record<string, string>>(
values: T,
fieldConfigs: import('../model/types').FieldConfig[],
crossValidators?: Record<string, (value: string, allValues: T) => string | null>,
): Record<string, string[]> {
const errors: Record<string, string[]> = {};
for (const config of fieldConfigs) {
const value = values[config.name] || '';
const fieldErrors = validateField(value, config.rules);
if (fieldErrors.length > 0) {
errors[config.name] = fieldErrors;
}
}
// 跨字段校验(如确认密码)
if (crossValidators) {
for (const [name, fn] of Object.entries(crossValidators)) {
const value = values[name] || '';
const err = fn(value, values);
if (err) {
errors[name] = [...(errors[name] || []), err];
}
}
}
return errors;
}