utils 工具函数
通用工具函数库,提供用户名处理、手机号验证、资料验证、成员等级等功能
概述
@mono/utils 是项目的通用工具函数库,提供了项目中常用的工具函数、验证逻辑和格式化功能。它包含用户名处理、手机号国际化支持、资料完整性验证、成员等级系统等核心功能。
为什么需要这个包?
虽然有很多第三方工具库,但我们仍然需要一个统一的工具包来:
- 业务逻辑封装 - 封装项目特定的业务规则和验证逻辑
- 类型安全 - 完整的 TypeScript 类型定义
- 国际化支持 - 支持多国家/地区的手机号格式
- 一致性 - 确保整个项目使用统一的工具函数
- 可维护性 - 集中管理常用功能,便于维护和更新
核心功能
1. 核心工具函数
提供 className 合并、URL 生成等基础工具。
className 合并
import { cn } from "@mono/utils";
// 合并 Tailwind CSS 类名,自动处理冲突
const className = cn("px-2 py-1", "px-4");
// 结果: "py-1 px-4" (px-4 覆盖 px-2)
// 支持条件类名
const buttonClass = cn(
"px-4 py-2 rounded",
isActive && "bg-blue-500",
isDisabled && "opacity-50"
);URL 生成
import { getBaseUrl } from "@mono/utils";
// 自动获取应用的基础 URL
const baseUrl = getBaseUrl();
// 生产环境: "https://example.com"
// 开发环境: "http://localhost:3000"
// 用于生成完整 URL
const shareUrl = `${getBaseUrl()}/events/${eventId}`;2. 用户名处理
提供用户名清理、验证和生成功能。
用户名清理
import { sanitizeUsername } from "@mono/utils";
// 清理用户输入,生成有效的用户名格式
sanitizeUsername("John Doe!"); // "john_doe"
sanitizeUsername("张三 123"); // "123"
sanitizeUsername("user@example.com"); // "userexamplecom"用户名验证
import { isValidUsername, RESERVED_USERNAMES } from "@mono/utils";
// 验证用户名是否有效
if (isValidUsername("john_doe")) {
console.log("用户名有效");
}
// 验证规则:
// - 长度 2-20 字符
// - 仅包含字母、数字、下划线
// - 不以下划线开头或结尾
// - 不在保留列表中
// 检查保留用户名
if (RESERVED_USERNAMES.includes("admin")) {
console.log("admin 是保留用户名");
}用户名生成
import { generateUsername } from "@mono/utils";
// 生成唯一用户名,自动处理冲突
const username = generateUsername("张三", ["zhangsan", "zhangsan1"]);
// 可能返回: "user_123456" (中文无法转换时的备选方案)
const username2 = generateUsername("John Doe", ["john_doe"]);
// 可能返回: "john_doe_dev" 或 "john_doe1"3. 显示名称验证
import { isValidDisplayName } from "@mono/utils";
// 验证显示名称
if (isValidDisplayName("张三")) {
console.log("显示名称有效");
}
// 验证规则:
// - 长度 1-50 字符
// - 不包含 < > " ` 等危险字符
// - 支持中文、英文、数字等4. 手机号格式化
支持多国家/地区的手机号格式化和标准化。
标准化手机号
import { normalizePhoneNumber } from "@mono/utils";
// 将各种格式统一为标准格式 (+国家代码+号码)
normalizePhoneNumber("138 1234 5678"); // "+8613812345678"
normalizePhoneNumber("8613812345678"); // "+8613812345678"
normalizePhoneNumber("+86 138-1234-5678"); // "+8613812345678"格式化显示
import { formatPhoneNumberForDisplay } from "@mono/utils";
// 格式化手机号用于显示
formatPhoneNumberForDisplay("+8613812345678");
// "+86 138 1234 5678"
formatPhoneNumberForDisplay("+12025551234");
// "+1 (202) 555-1234"
formatPhoneNumberForDisplay("+4420712345678");
// "+4420712345678" (其他国家保持原格式)提取信息
import {
extractCountryCode,
extractPhoneNumber,
isStandardPhoneNumber,
} from "@mono/utils";
// 提取国家代码
extractCountryCode("+8613812345678"); // "+86"
// 提取本地号码
extractPhoneNumber("+8613812345678"); // "13812345678"
// 检查是否为标准格式
isStandardPhoneNumber("+8613812345678"); // true
isStandardPhoneNumber("13812345678"); // false5. 手机号验证
提供严格的手机号验证,支持多个国家/地区。
基础验证
import { validatePhoneNumber } from "@mono/utils";
// 验证手机号格���
const result = validatePhoneNumber("+86", "13800138000");
if (result.isValid) {
console.log("手机号有效");
} else {
console.log(result.errorMessage); // "中国大陆手机号至少需要11位数字"
console.log(result.suggestion); // "示例: 13800138000"
}支持的国家/地区
// 中国大陆 (+86)
validatePhoneNumber("+86", "13800138000"); // ✅
// 香港 (+852)
validatePhoneNumber("+852", "91234567"); // ✅
// 澳门 (+853)
validatePhoneNumber("+853", "61234567"); // ✅
// 台湾 (+886)
validatePhoneNumber("+886", "912345678"); // ✅
// 美国/加拿大 (+1)
validatePhoneNumber("+1", "2025551234"); // ✅
// 新加坡 (+65)
validatePhoneNumber("+65", "91234567"); // ✅
// 英国 (+44)
validatePhoneNumber("+44", "7912345678"); // ✅
// 日本 (+81)
validatePhoneNumber("+81", "9012345678"); // ✅
// 韩国 (+82)
validatePhoneNumber("+82", "1012345678"); // ✅完整手机号验证
import { validateFullPhoneNumber } from "@mono/utils";
// 验证包含国家代码的完整手机号
const result = validateFullPhoneNumber("+8613800138000");
if (result.isValid) {
console.log("手机号有效");
}
// 自动识别国家代码
validateFullPhoneNumber("+12025551234"); // 美国号码
validateFullPhoneNumber("+8529123456"); // 香港号码获取验证规则
import { getPhoneValidationRule } from "@mono/utils";
// 获取国家的验证规则
const rule = getPhoneValidationRule("+86");
console.log(rule.countryName); // "中国大陆"
console.log(rule.minLength); // 11
console.log(rule.maxLength); // 11
console.log(rule.example); // "13800138000"6. 资料验证
提供用户资料完整性验证功能。
核心资料验证
import { validateCoreProfile } from "@mono/utils";
const user = {
name: "张三",
email: "zhangsan@example.com",
phoneNumber: "+8613800138000",
bio: "一个热爱编程的开发者",
userRoleString: "全栈工程师",
currentWorkOn: "开发 MVP 项目",
lifeStatus: "在职",
};
const validation = validateCoreProfile(user);
console.log(validation.isComplete); // true/false
console.log(validation.completionPercentage); // 85
console.log(validation.missingFields); // ["个人简介", "当前在做"]
console.log(validation.missingCount); // 2必填字段
核心资料的必填字段包括:
- 姓名
- 手机号
- 邮箱
- 个人简介
- 主要角色
- 当前在做
- 当前状态
推荐字段
import { getMissingRecommendedProfileFields } from "@mono/utils";
// 获取缺失的推荐字段
const missing = getMissingRecommendedProfileFields(user);
// ["微信号", "技能标签", "可以提供的帮助", "寻找什么"]组织申请验证
import { validateProfileForOrganizationApplication } from "@mono/utils";
// 验证组织申请所需的资料 (比核心资料要求更多)
const validation = validateProfileForOrganizationApplication(user);
// 额外必填字段:
// - 用户名
// - 地区
// - 性别
// - 微信号生成提示消息
import { getProfileCompletionMessage } from "@mono/utils";
const validation = validateCoreProfile(user);
const message = getProfileCompletionMessage(validation);
console.log(message.title); // "资料待完善"
console.log(message.description); // "还需要完善 2 项信息,包括:个人简介、当前在做"
console.log(message.actionText); // "完善资料"7. 成员等级系统
基于贡献值 (CP) 的成员等级系统。
获取成员等级
import { getMemberLevel } from "@mono/utils";
const level = getMemberLevel(50);
console.log(level.level); // 3
console.log(level.name); // "贡献者"
console.log(level.color); // "text-blue-600 bg-blue-100"
console.log(level.badge); // "⭐"等级划分
| CP 值 | 等级 | 名称 | 徽章 |
|---|---|---|---|
| 0 | 0 | 观众 | 👀 |
| 1-9 | 1 | 注册用户 | 👋 |
| 10-49 | 2 | 社区成员 | 🔥 |
| 50-199 | 3 | 贡献者 | ⭐ |
| 200+ | 4 | 核心贡献者 | 🌟 |
权限检查
import { canViewMemberQrCode } from "@mono/utils";
// 检查是否可以查看成员二维码 (需要社区成员及以上)
if (canViewMemberQrCode(user.cpValue)) {
console.log("可以查看二维码");
}8. 常量配置
提供项目中常用的常量配置。
城市列表
import { CITIES } from "@mono/utils";
// 城市选项
console.log(CITIES);
// ["北京", "上海", "深圳", "广州", "杭州", "成都", ...]标签预设
import { TAG_PRESETS, getAllTags } from "@mono/utils";
// 按类别获取标签
console.log(TAG_PRESETS.技术领域);
// ["AI", "Web3", "硬件", "独立开发", "设计", "出海", "开源"]
console.log(TAG_PRESETS.活动类型);
// ["黑客松", "线下聚会", "线上分享", "工作坊", ...]
// 获取所有标签
const allTags = getAllTags();
// ["AI", "Web3", ..., "黑客松", ..., "教育", ...]实战示例
示例 1: 用户注册表单
import {
sanitizeUsername,
isValidUsername,
validatePhoneNumber,
} from "@mono/utils";
import { useState } from "react";
export function RegisterForm() {
const [username, setUsername] = useState("");
const [phone, setPhone] = useState("");
const [errors, setErrors] = useState<Record<string, string>>({});
const handleUsernameChange = (value: string) => {
const sanitized = sanitizeUsername(value);
setUsername(sanitized);
if (!isValidUsername(sanitized)) {
setErrors((prev) => ({
...prev,
username: "用户名格式不正确 (2-20字符,仅字母数字下划线)",
}));
} else {
setErrors((prev) => {
const { username, ...rest } = prev;
return rest;
});
}
};
const handlePhoneChange = (value: string) => {
setPhone(value);
const result = validatePhoneNumber("+86", value);
if (!result.isValid) {
setErrors((prev) => ({
...prev,
phone: result.errorMessage || "手机号格式不正确",
}));
} else {
setErrors((prev) => {
const { phone, ...rest } = prev;
return rest;
});
}
};
return (
<form>
<div>
<input
value={username}
onChange={(e) => handleUsernameChange(e.target.value)}
placeholder="用户名"
/>
{errors.username && <p className="error">{errors.username}</p>}
</div>
<div>
<input
value={phone}
onChange={(e) => handlePhoneChange(e.target.value)}
placeholder="手机号"
/>
{errors.phone && <p className="error">{errors.phone}</p>}
</div>
<button type="submit" disabled={Object.keys(errors).length > 0}>
注册
</button>
</form>
);
}示例 2: 资料完善提示
import {
validateCoreProfile,
getProfileCompletionMessage,
} from "@mono/utils";
export function ProfileCompletionBanner({ user }) {
const validation = validateCoreProfile(user);
// 资料完整时不显示
if (validation.isComplete) {
return null;
}
const message = getProfileCompletionMessage(validation);
return (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="font-semibold text-yellow-900">{message.title}</h3>
<p className="text-sm text-yellow-700 mt-1">{message.description}</p>
{/* 进度条 */}
<div className="mt-3 bg-yellow-200 rounded-full h-2">
<div
style={{ width: `${validation.completionPercentage}%` }}
className="bg-yellow-500 h-2 rounded-full transition-all"
/>
</div>
<p className="text-xs text-yellow-600 mt-1">
已完成 {validation.completedCount}/{validation.totalRequiredFields} 项
</p>
</div>
<a
href="/profile/edit"
className="ml-4 px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
>
{message.actionText}
</a>
</div>
</div>
);
}示例 3: 手机号输入组件
import {
normalizePhoneNumber,
formatPhoneNumberForDisplay,
validatePhoneNumber,
} from "@mono/utils";
import { useState } from "react";
interface PhoneInputProps {
value: string;
onChange: (value: string) => void;
countryCode?: string;
}
export function PhoneInput({
value,
onChange,
countryCode = "+86",
}: PhoneInputProps) {
const [displayValue, setDisplayValue] = useState(
formatPhoneNumberForDisplay(value)
);
const [error, setError] = useState("");
const handleChange = (input: string) => {
// 更新显示值
setDisplayValue(input);
// 标准化并验证
const normalized = normalizePhoneNumber(input);
const phoneNumber = normalized.replace(countryCode, "");
const validation = validatePhoneNumber(countryCode, phoneNumber);
if (validation.isValid) {
onChange(normalized);
setError("");
} else {
setError(validation.errorMessage || "");
}
};
const handleBlur = () => {
// 失焦时格式化显示
if (value) {
setDisplayValue(formatPhoneNumberForDisplay(value));
}
};
return (
<div>
<input
type="tel"
value={displayValue}
onChange={(e) => handleChange(e.target.value)}
onBlur={handleBlur}
placeholder="请输入手机号"
className={error ? "border-red-500" : ""}
/>
{error && <p className="text-sm text-red-600 mt-1">{error}</p>}
</div>
);
}示例 4: 成员等级展示
import { getMemberLevel, canViewMemberQrCode } from "@mono/utils";
export function MemberBadge({ cpValue }: { cpValue: number }) {
const level = getMemberLevel(cpValue);
return (
<div className="flex items-center gap-3">
<div className={`px-3 py-1 rounded-full ${level.color}`}>
<span className="mr-1">{level.badge}</span>
<span className="text-sm font-medium">{level.name}</span>
</div>
{canViewMemberQrCode(cpValue) && (
<button className="text-sm text-blue-600 hover:underline">
查看二维码
</button>
)}
</div>
);
}示例 5: 组织申请检查
import { validateProfileForOrganizationApplication } from "@mono/utils";
import { redirect } from "next/navigation";
export default async function OrganizationApplicationPage() {
const user = await getUser();
const validation = validateProfileForOrganizationApplication(user);
if (!validation.isComplete) {
return (
<div className="max-w-2xl mx-auto p-6">
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h2 className="text-xl font-semibold text-red-900 mb-2">
资料不完整
</h2>
<p className="text-red-700 mb-4">
申请创建组织需要完善以下信息:
</p>
<ul className="list-disc list-inside space-y-1 text-red-700">
{validation.missingFields.map((field) => (
<li key={field}>{field}</li>
))}
</ul>
<a
href="/profile/edit"
className="mt-4 inline-block px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
完善资料
</a>
</div>
</div>
);
}
return <OrganizationApplicationForm />;
}最佳实践
1. 用户名生成
始终使用 generateUsername 而不是手动拼接:
// ✅ 推荐
const username = generateUsername(user.name, existingUsernames);
// ❌ 不推荐
const username = user.name.toLowerCase().replace(/\s/g, "_");2. 手机号处理
存储标准化格式,显示格式化版本:
// ✅ 推荐
// 存储
const normalized = normalizePhoneNumber(input);
await db.user.update({ phoneNumber: normalized });
// 显示
const display = formatPhoneNumberForDisplay(user.phoneNumber);
// ❌ 不推荐
// 直接存储用户输入
await db.user.update({ phoneNumber: input });3. 资料验证
在多个场景使用统一的验证逻辑:
// ✅ 推荐
const validation = validateCoreProfile(user);
if (!validation.isComplete) {
return <ProfileCompletionPrompt validation={validation} />;
}
// ❌ 不推荐
// 在每个组件中重复验证逻辑
if (!user.name || !user.email || !user.phoneNumber) {
// ...
}4. 错误提示
使用验证结果中的建议信息:
// ✅ 推荐
const result = validatePhoneNumber("+86", phone);
if (!result.isValid) {
showError(result.errorMessage);
if (result.suggestion) {
showHint(result.suggestion);
}
}
// ❌ 不推荐
if (!isValidPhone(phone)) {
showError("手机号格式不正确");
}5. 成员等级展示
使用预定义的颜色和徽章:
// ✅ 推荐
const level = getMemberLevel(user.cpValue);
return <div className={level.color}>{level.badge} {level.name}</div>;
// ❌ 不推荐
// 硬编码等级逻辑
const levelName = user.cpValue >= 50 ? "贡献者" : "成员";故障排查
用户名验证失败
问题: isValidUsername() 返回 false
解决方案:
- 检查长度是否在 2-20 字符之间
- 确认不包含特殊字符
- 检查是否在保留列表中
// 调试代码
console.log("Username:", username);
console.log("Length:", username.length);
console.log("Is reserved:", RESERVED_USERNAMES.includes(username.toLowerCase()));手机号格式化错误
问题: normalizePhoneNumber() 返回意外结果
解决方案: 确保输入包含足够的数字位数
// ✅ 正确
normalizePhoneNumber("13812345678"); // "+8613812345678"
// ❌ 错误
normalizePhoneNumber("138"); // "+138" (不完整)资料验证不准确
问题: validateCoreProfile() 结果不符合预期
解决方案: 检查用户对象是否包含所有必需字段
// 调试代码
const validation = validateCoreProfile(user);
console.log("Missing fields:", validation.missingFields);
console.log("User object:", user);手机号验证失败
问题: 有效的手机号被标记为无效
解决方案:
- 确认国家代码正确
- 检查号码长度
- 验证号码格式
// 调试代码
const result = validatePhoneNumber("+86", phone);
console.log("Validation result:", result);
const rule = getPhoneValidationRule("+86");
console.log("Validation rule:", rule);API 参考
完整的 API 文档请参考 README.md。