develop (#68)
* docs(AGENTS.md): 添加 Git Commit 必须使用中文描述的规范 * docs: 更新测试数据生成器功能设计文档 * refactor: 删除死代码 * docs: 统一测试数据生成器文档,移除 SQL/TypeScript 导出,修复接口命名不一致 * feat: 实现测试数据生成器功能 * docs: 更新 README 和 AGENTS 文档,补充测试数据生成器模块说明
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* 基础类型生成器
|
||||
* 包含:随机整数、随机浮点数、随机字符串、从列表选择
|
||||
*/
|
||||
|
||||
import type { GeneratorDefinition } from '@/types/testDataGenerator';
|
||||
|
||||
/**
|
||||
* 生成随机整数
|
||||
*/
|
||||
function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中随机选择
|
||||
*/
|
||||
function randomPick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机整数生成器
|
||||
*/
|
||||
export const randomIntGenerator: GeneratorDefinition = {
|
||||
id: 'randomInt',
|
||||
name: '随机整数',
|
||||
description: '生成指定范围内的随机整数',
|
||||
categoryId: 'basic',
|
||||
params: [
|
||||
{
|
||||
key: 'min',
|
||||
label: '最小值',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
description: '整数最小值',
|
||||
},
|
||||
{
|
||||
key: 'max',
|
||||
label: '最大值',
|
||||
type: 'number',
|
||||
defaultValue: 100,
|
||||
description: '整数最大值',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const min = (params.min as number) || 0;
|
||||
const max = (params.max as number) || 100;
|
||||
return String(randomInt(min, max));
|
||||
},
|
||||
generateAtIndex: (params, index) => {
|
||||
const min = (params.min as number) || 0;
|
||||
const max = (params.max as number) || 100;
|
||||
const range = max - min + 1;
|
||||
return String(min + (index % range));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 随机浮点数生成器
|
||||
*/
|
||||
export const randomFloat: GeneratorDefinition = {
|
||||
id: 'randomFloat',
|
||||
name: '随机浮点数',
|
||||
description: '生成指定范围内的随机浮点数',
|
||||
categoryId: 'basic',
|
||||
params: [
|
||||
{
|
||||
key: 'min',
|
||||
label: '最小值',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
description: '浮点数最小值',
|
||||
},
|
||||
{
|
||||
key: 'max',
|
||||
label: '最大值',
|
||||
type: 'number',
|
||||
defaultValue: 100,
|
||||
description: '浮点数最大值',
|
||||
},
|
||||
{
|
||||
key: 'decimals',
|
||||
label: '小数位数',
|
||||
type: 'number',
|
||||
defaultValue: 2,
|
||||
min: 0,
|
||||
max: 10,
|
||||
description: '小数位数',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const min = (params.min as number) || 0;
|
||||
const max = (params.max as number) || 100;
|
||||
const decimals = (params.decimals as number) ?? 2;
|
||||
const value = min + Math.random() * (max - min);
|
||||
return value.toFixed(decimals);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 随机字符串生成器
|
||||
*/
|
||||
export const randomString: GeneratorDefinition = {
|
||||
id: 'randomString',
|
||||
name: '随机字符串',
|
||||
description: '生成指定长度的随机字符串',
|
||||
categoryId: 'basic',
|
||||
params: [
|
||||
{
|
||||
key: 'length',
|
||||
label: '字符串长度',
|
||||
type: 'number',
|
||||
defaultValue: 10,
|
||||
min: 1,
|
||||
max: 100,
|
||||
description: '字符串长度',
|
||||
},
|
||||
{
|
||||
key: 'charset',
|
||||
label: '字符集',
|
||||
type: 'select',
|
||||
defaultValue: 'alphanumeric',
|
||||
description: '使用的字符集',
|
||||
options: [
|
||||
{ label: '字母数字', value: 'alphanumeric' },
|
||||
{ label: '仅字母', value: 'alpha' },
|
||||
{ label: '仅数字', value: 'numeric' },
|
||||
{ label: '小写字母', value: 'lowercase' },
|
||||
{ label: '大写字母', value: 'uppercase' },
|
||||
{ label: '十六进制', value: 'hex' },
|
||||
{ label: 'Base64', value: 'base64' },
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const length = (params.length as number) || 10;
|
||||
const charset = (params.charset as string) || 'alphanumeric';
|
||||
|
||||
const charsets: Record<string, string> = {
|
||||
alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
||||
alpha: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
numeric: '0123456789',
|
||||
lowercase: 'abcdefghijklmnopqrstuvwxyz',
|
||||
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
hex: '0123456789abcdef',
|
||||
base64: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
|
||||
};
|
||||
|
||||
const chars = charsets[charset] || charsets.alphanumeric;
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
generateAtIndex: (params, index) => {
|
||||
const length = (params.length as number) || 10;
|
||||
const charset = (params.charset as string) || 'alphanumeric';
|
||||
|
||||
const charsets: Record<string, string> = {
|
||||
alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
||||
alpha: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
||||
numeric: '0123456789',
|
||||
lowercase: 'abcdefghijklmnopqrstuvwxyz',
|
||||
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
hex: '0123456789abcdef',
|
||||
base64: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
|
||||
};
|
||||
|
||||
const chars = charsets[charset] || charsets.alphanumeric;
|
||||
const base = String(index).padStart(length, '0');
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const charIndex = parseInt(base[i]) || 0;
|
||||
result += chars[charIndex % chars.length];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 从列表选择生成器
|
||||
*/
|
||||
export const fromList: GeneratorDefinition = {
|
||||
id: 'fromList',
|
||||
name: '从列表选择',
|
||||
description: '从自定义列表中随机选择',
|
||||
categoryId: 'basic',
|
||||
params: [
|
||||
{
|
||||
key: 'values',
|
||||
label: '列表值',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
description: '列表值,用逗号分隔',
|
||||
placeholder: '值1,值2,值3',
|
||||
},
|
||||
{
|
||||
key: 'allowDuplicate',
|
||||
label: '允许重复',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
description: '是否允许重复选择',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const valuesStr = (params.values as string) || '';
|
||||
const values = valuesStr
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v);
|
||||
|
||||
if (values.length === 0) {
|
||||
return '(请配置列表值)';
|
||||
}
|
||||
|
||||
return randomPick(values);
|
||||
},
|
||||
};
|
||||
|
||||
export const basicGenerators: GeneratorDefinition[] = [
|
||||
randomIntGenerator,
|
||||
randomFloat,
|
||||
randomString,
|
||||
fromList,
|
||||
];
|
||||
@@ -0,0 +1,561 @@
|
||||
/**
|
||||
* 业务数据生成器
|
||||
* 包含:订单号、价格、日期、状态、数量、评分、折扣、库存
|
||||
*/
|
||||
|
||||
import type { GeneratorDefinition } from '@/types/testDataGenerator';
|
||||
|
||||
/**
|
||||
* 生成随机整数
|
||||
*/
|
||||
function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中随机选择
|
||||
*/
|
||||
function randomPick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
function formatDate(date: Date, format: string): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单号生成器
|
||||
*/
|
||||
export const orderId: GeneratorDefinition = {
|
||||
id: 'orderId',
|
||||
name: '订单号',
|
||||
description: '生成随机订单号',
|
||||
categoryId: 'business',
|
||||
params: [
|
||||
{
|
||||
key: 'prefix',
|
||||
label: '前缀',
|
||||
type: 'string',
|
||||
defaultValue: 'ORD',
|
||||
description: '订单号前缀',
|
||||
placeholder: '请输入前缀',
|
||||
},
|
||||
{
|
||||
key: 'length',
|
||||
label: '数字长度',
|
||||
type: 'number',
|
||||
defaultValue: 10,
|
||||
min: 6,
|
||||
max: 20,
|
||||
description: '订单号数字部分长度',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const prefix = (params.prefix as string) || 'ORD';
|
||||
const length = (params.length as number) || 10;
|
||||
const maxNum = Math.pow(10, length) - 1;
|
||||
const num = String(randomInt(0, maxNum)).padStart(length, '0');
|
||||
return `${prefix}${num}`;
|
||||
},
|
||||
generateAtIndex: (params, index) => {
|
||||
const prefix = (params.prefix as string) || 'ORD';
|
||||
const length = (params.length as number) || 10;
|
||||
const num = String(index + 1).padStart(length, '0');
|
||||
return `${prefix}${num}`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 价格生成器
|
||||
*/
|
||||
export const price: GeneratorDefinition = {
|
||||
id: 'price',
|
||||
name: '价格',
|
||||
description: '生成随机价格',
|
||||
categoryId: 'business',
|
||||
params: [
|
||||
{
|
||||
key: 'min',
|
||||
label: '最低价格',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
min: 0,
|
||||
description: '价格最小值',
|
||||
},
|
||||
{
|
||||
key: 'max',
|
||||
label: '最高价格',
|
||||
type: 'number',
|
||||
defaultValue: 1000,
|
||||
min: 0,
|
||||
description: '价格最大值',
|
||||
},
|
||||
{
|
||||
key: 'decimals',
|
||||
label: '小数位数',
|
||||
type: 'number',
|
||||
defaultValue: 2,
|
||||
min: 0,
|
||||
max: 4,
|
||||
description: '价格小数位数',
|
||||
},
|
||||
{
|
||||
key: 'strategy',
|
||||
label: '生成策略',
|
||||
type: 'select',
|
||||
defaultValue: 'random',
|
||||
description: '价格生成策略',
|
||||
options: [
|
||||
{ label: '随机', value: 'random' },
|
||||
{ label: '心理定价', value: 'psychological' },
|
||||
{ label: '真实分布', value: 'realistic' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'currency',
|
||||
label: '货币符号',
|
||||
type: 'string',
|
||||
defaultValue: '¥',
|
||||
description: '货币符号',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const min = (params.min as number) || 1;
|
||||
const max = (params.max as number) || 1000;
|
||||
const decimals = (params.decimals as number) ?? 2;
|
||||
const strategy = (params.strategy as string) || 'random';
|
||||
const currency = (params.currency as string) || '¥';
|
||||
|
||||
let value: number;
|
||||
if (strategy === 'psychological') {
|
||||
value = generatePsychologicalPrice(min, max);
|
||||
} else if (strategy === 'realistic') {
|
||||
value = generateRealisticPrice(min, max);
|
||||
} else {
|
||||
value = min + Math.random() * (max - min);
|
||||
}
|
||||
|
||||
return `${currency}${value.toFixed(decimals)}`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成心理定价(如 9.99, 19.9, 99)
|
||||
*/
|
||||
function generatePsychologicalPrice(min: number, max: number): number {
|
||||
const patterns = [
|
||||
() => {
|
||||
const base = randomInt(Math.ceil(min), Math.floor(max / 10));
|
||||
return base * 10 - 0.01;
|
||||
},
|
||||
() => {
|
||||
const base = randomInt(Math.ceil(min / 10), Math.floor(max / 10));
|
||||
return base * 10 - 1;
|
||||
},
|
||||
() => {
|
||||
const base = randomInt(Math.ceil(min / 100), Math.floor(max / 100));
|
||||
return base * 100 - 1;
|
||||
},
|
||||
];
|
||||
|
||||
let price = randomPick(patterns)();
|
||||
if (price < min) price = min;
|
||||
if (price > max) price = max;
|
||||
return price;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成真实分布价格(对数正态分布)
|
||||
*/
|
||||
function generateRealisticPrice(min: number, max: number): number {
|
||||
const mean = (Math.log(min) + Math.log(max)) / 2;
|
||||
const stdDev = (Math.log(max) - Math.log(min)) / 4;
|
||||
let u = 0,
|
||||
v = 0;
|
||||
while (u === 0) u = Math.random();
|
||||
while (v === 0) v = Math.random();
|
||||
const num = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
||||
const value = Math.exp(mean + num * stdDev);
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期生成器
|
||||
*/
|
||||
export const date: GeneratorDefinition = {
|
||||
id: 'date',
|
||||
name: '日期',
|
||||
description: '生成随机日期',
|
||||
categoryId: 'business',
|
||||
params: [
|
||||
{
|
||||
key: 'startDate',
|
||||
label: '开始日期',
|
||||
type: 'string',
|
||||
defaultValue: '2020-01-01',
|
||||
description: '日期范围开始',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
},
|
||||
{
|
||||
key: 'endDate',
|
||||
label: '结束日期',
|
||||
type: 'string',
|
||||
defaultValue: '2025-12-31',
|
||||
description: '日期范围结束',
|
||||
placeholder: 'YYYY-MM-DD',
|
||||
},
|
||||
{
|
||||
key: 'format',
|
||||
label: '日期格式',
|
||||
type: 'select',
|
||||
defaultValue: 'YYYY-MM-DD',
|
||||
description: '输出格式',
|
||||
options: [
|
||||
{ label: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },
|
||||
{ label: 'YYYY/MM/DD', value: 'YYYY/MM/DD' },
|
||||
{ label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss' },
|
||||
{ label: 'YYYY/MM/DD HH:mm:ss', value: 'YYYY/MM/DD HH:mm:ss' },
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const startStr = (params.startDate as string) || '2020-01-01';
|
||||
const endStr = (params.endDate as string) || '2025-12-31';
|
||||
const format = (params.format as string) || 'YYYY-MM-DD';
|
||||
|
||||
const start = new Date(startStr).getTime();
|
||||
const end = new Date(endStr).getTime();
|
||||
const randomTime = start + Math.random() * (end - start);
|
||||
const date = new Date(randomTime);
|
||||
|
||||
return formatDate(date, format);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 状态生成器
|
||||
*/
|
||||
export const status: GeneratorDefinition = {
|
||||
id: 'status',
|
||||
name: '状态',
|
||||
description: '生成随机状态',
|
||||
categoryId: 'business',
|
||||
params: [
|
||||
{
|
||||
key: 'type',
|
||||
label: '状态类型',
|
||||
type: 'select',
|
||||
defaultValue: 'order',
|
||||
description: '选择状态类型',
|
||||
options: [
|
||||
{ label: '订单状态', value: 'order' },
|
||||
{ label: '用户状态', value: 'user' },
|
||||
{ label: '审核状态', value: 'review' },
|
||||
{ label: '支付状态', value: 'payment' },
|
||||
{ label: '自定义', value: 'custom' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'customValues',
|
||||
label: '自定义值',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
description: '自定义状态值,用逗号分隔',
|
||||
placeholder: '状态1,状态2,状态3',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const type = (params.type as string) || 'order';
|
||||
|
||||
const statusMap: Record<string, string[]> = {
|
||||
order: ['待付款', '待发货', '已发货', '已完成', '已取消', '已退款'],
|
||||
user: ['活跃', '未激活', '已禁用', '已注销'],
|
||||
review: ['待审核', '审核中', '已通过', '已拒绝'],
|
||||
payment: ['待支付', '支付中', '支付成功', '支付失败', '已退款'],
|
||||
};
|
||||
|
||||
if (type === 'custom') {
|
||||
const customValues = (params.customValues as string) || '';
|
||||
const values = customValues.split(',').filter((v) => v.trim());
|
||||
if (values.length > 0) {
|
||||
return randomPick(values);
|
||||
}
|
||||
return '未知状态';
|
||||
}
|
||||
|
||||
return randomPick(statusMap[type] || statusMap.order);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 数量生成器
|
||||
*/
|
||||
export const quantity: GeneratorDefinition = {
|
||||
id: 'quantity',
|
||||
name: '数量',
|
||||
description: '生成随机数量',
|
||||
categoryId: 'business',
|
||||
params: [
|
||||
{
|
||||
key: 'min',
|
||||
label: '最小值',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
min: 0,
|
||||
description: '数量最小值',
|
||||
},
|
||||
{
|
||||
key: 'max',
|
||||
label: '最大值',
|
||||
type: 'number',
|
||||
defaultValue: 100,
|
||||
min: 0,
|
||||
description: '数量最大值',
|
||||
},
|
||||
{
|
||||
key: 'distribution',
|
||||
label: '分布方式',
|
||||
type: 'select',
|
||||
defaultValue: 'uniform',
|
||||
description: '数量分布方式',
|
||||
options: [
|
||||
{ label: '均匀分布', value: 'uniform' },
|
||||
{ label: '泊松分布', value: 'poisson' },
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const min = (params.min as number) || 1;
|
||||
const max = (params.max as number) || 100;
|
||||
const distribution = (params.distribution as string) || 'uniform';
|
||||
|
||||
if (distribution === 'poisson') {
|
||||
return String(poissonRandom(Math.floor((min + max) / 2)));
|
||||
}
|
||||
return String(randomInt(min, max));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 泊松分布随机数
|
||||
*/
|
||||
function poissonRandom(lambda: number): number {
|
||||
const L = Math.exp(-lambda);
|
||||
let k = 0;
|
||||
let p = 1;
|
||||
do {
|
||||
k++;
|
||||
p *= Math.random();
|
||||
} while (p > L);
|
||||
return k - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评分生成器
|
||||
*/
|
||||
export const rating: GeneratorDefinition = {
|
||||
id: 'rating',
|
||||
name: '评分',
|
||||
description: '生成随机评分',
|
||||
categoryId: 'business',
|
||||
params: [
|
||||
{
|
||||
key: 'min',
|
||||
label: '最低分',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
min: 0,
|
||||
max: 10,
|
||||
description: '评分最小值',
|
||||
},
|
||||
{
|
||||
key: 'max',
|
||||
label: '最高分',
|
||||
type: 'number',
|
||||
defaultValue: 5,
|
||||
min: 0,
|
||||
max: 10,
|
||||
description: '评分最大值',
|
||||
},
|
||||
{
|
||||
key: 'decimals',
|
||||
label: '小数位数',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
min: 0,
|
||||
max: 2,
|
||||
description: '评分小数位数',
|
||||
},
|
||||
{
|
||||
key: 'skewed',
|
||||
label: '偏向高分',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
description: '是否偏向高分(模拟真实评分分布)',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const min = (params.min as number) || 1;
|
||||
const max = (params.max as number) || 5;
|
||||
const decimals = (params.decimals as number) ?? 1;
|
||||
const skewed = params.skewed !== false;
|
||||
|
||||
if (skewed) {
|
||||
return skewedRandom(min, max).toFixed(decimals);
|
||||
}
|
||||
return (min + Math.random() * (max - min)).toFixed(decimals);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 偏向高分的随机数(模拟真实评分分布)
|
||||
*/
|
||||
function skewedRandom(min: number, max: number): number {
|
||||
// 使用 Beta 分布模拟评分偏向
|
||||
const alpha = 3;
|
||||
const beta = 1;
|
||||
let u = 0,
|
||||
v = 0;
|
||||
while (u === 0) u = Math.random();
|
||||
while (v === 0) v = Math.random();
|
||||
const x = Math.pow(u, 1 / alpha) / Math.pow(u, 1 / alpha) + Math.pow(v, 1 / beta);
|
||||
return min + x * (max - min);
|
||||
}
|
||||
|
||||
/**
|
||||
* 折扣生成器
|
||||
*/
|
||||
export const discount: GeneratorDefinition = {
|
||||
id: 'discount',
|
||||
name: '折扣',
|
||||
description: '生成随机折扣',
|
||||
categoryId: 'business',
|
||||
params: [
|
||||
{
|
||||
key: 'min',
|
||||
label: '最低折扣',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
min: 0,
|
||||
max: 100,
|
||||
description: '折扣最小值(百分比)',
|
||||
},
|
||||
{
|
||||
key: 'max',
|
||||
label: '最高折扣',
|
||||
type: 'number',
|
||||
defaultValue: 50,
|
||||
min: 0,
|
||||
max: 100,
|
||||
description: '折扣最大值(百分比)',
|
||||
},
|
||||
{
|
||||
key: 'strategy',
|
||||
label: '生成策略',
|
||||
type: 'select',
|
||||
defaultValue: 'random',
|
||||
description: '折扣生成策略',
|
||||
options: [
|
||||
{ label: '随机', value: 'random' },
|
||||
{ label: '心理定价', value: 'psychological' },
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const min = (params.min as number) || 1;
|
||||
const max = (params.max as number) || 50;
|
||||
const strategy = (params.strategy as string) || 'random';
|
||||
|
||||
let discount: number;
|
||||
if (strategy === 'psychological') {
|
||||
discount = generatePsychologicalDiscount(min, max);
|
||||
} else {
|
||||
discount = randomInt(min, max);
|
||||
}
|
||||
return `${discount}%`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成心理折扣(如 5%, 10%, 20%, 30%)
|
||||
*/
|
||||
function generatePsychologicalDiscount(min: number, max: number): number {
|
||||
const psychologicalDiscounts = [5, 10, 15, 20, 25, 30, 40, 50, 60, 70, 80];
|
||||
const validDiscounts = psychologicalDiscounts.filter((d) => d >= min && d <= max);
|
||||
if (validDiscounts.length > 0) {
|
||||
return randomPick(validDiscounts);
|
||||
}
|
||||
return randomInt(min, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存生成器
|
||||
*/
|
||||
export const stock: GeneratorDefinition = {
|
||||
id: 'stock',
|
||||
name: '库存',
|
||||
description: '生成随机库存数量',
|
||||
categoryId: 'business',
|
||||
params: [
|
||||
{
|
||||
key: 'min',
|
||||
label: '最小库存',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
min: 0,
|
||||
description: '库存最小值',
|
||||
},
|
||||
{
|
||||
key: 'max',
|
||||
label: '最大库存',
|
||||
type: 'number',
|
||||
defaultValue: 1000,
|
||||
min: 0,
|
||||
description: '库存最大值',
|
||||
},
|
||||
{
|
||||
key: 'allowZero',
|
||||
label: '允许为零',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
description: '是否允许库存为零',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const min = (params.min as number) || 0;
|
||||
const max = (params.max as number) || 1000;
|
||||
const allowZero = params.allowZero !== false;
|
||||
|
||||
if (allowZero) {
|
||||
return String(randomInt(min, max));
|
||||
}
|
||||
return String(randomInt(Math.max(1, min), max));
|
||||
},
|
||||
};
|
||||
|
||||
export const businessGenerators: GeneratorDefinition[] = [
|
||||
orderId,
|
||||
price,
|
||||
date,
|
||||
status,
|
||||
quantity,
|
||||
rating,
|
||||
discount,
|
||||
stock,
|
||||
];
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 生成器库索引
|
||||
* 导出所有生成器和分类配置
|
||||
*/
|
||||
|
||||
import type { GeneratorDefinition, GeneratorCategory } from '@/types/testDataGenerator';
|
||||
import { personalGenerators } from './personal';
|
||||
import { businessGenerators } from './business';
|
||||
import { technicalGenerators } from './technical';
|
||||
import { basicGenerators } from './basic';
|
||||
|
||||
/**
|
||||
* 生成器分类配置
|
||||
*/
|
||||
export const generatorCategories: GeneratorCategory[] = [
|
||||
{ id: 'personal', name: '个人信息', icon: 'User' },
|
||||
{ id: 'business', name: '业务数据', icon: 'Briefcase' },
|
||||
{ id: 'technical', name: '技术数据', icon: 'Code' },
|
||||
{ id: 'basic', name: '基础类型', icon: 'Hash' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 所有生成器列表
|
||||
*/
|
||||
export const allGenerators: GeneratorDefinition[] = [
|
||||
...personalGenerators,
|
||||
...businessGenerators,
|
||||
...technicalGenerators,
|
||||
...basicGenerators,
|
||||
];
|
||||
|
||||
/**
|
||||
* 根据 ID 获取生成器
|
||||
*/
|
||||
export function getGeneratorById(id: string): GeneratorDefinition | undefined {
|
||||
return allGenerators.find((g) => g.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类 ID 获取生成器列表
|
||||
*/
|
||||
export function getGeneratorsByCategory(categoryId: string): GeneratorDefinition[] {
|
||||
return allGenerators.filter((g) => g.categoryId === categoryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索生成器
|
||||
*/
|
||||
export function searchGenerators(query: string): GeneratorDefinition[] {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return allGenerators.filter(
|
||||
(g) =>
|
||||
g.name.toLowerCase().includes(lowerQuery) ||
|
||||
g.description.toLowerCase().includes(lowerQuery) ||
|
||||
g.id.toLowerCase().includes(lowerQuery),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* 个人信息生成器
|
||||
* 包含:中文姓名、邮箱、手机号、身份证号、中文地址、年龄
|
||||
*/
|
||||
|
||||
import type { GeneratorDefinition } from '@/types/testDataGenerator';
|
||||
|
||||
// 中文姓氏
|
||||
const SURNAMES = [
|
||||
'王',
|
||||
'李',
|
||||
'张',
|
||||
'刘',
|
||||
'陈',
|
||||
'杨',
|
||||
'赵',
|
||||
'黄',
|
||||
'周',
|
||||
'吴',
|
||||
'徐',
|
||||
'孙',
|
||||
'胡',
|
||||
'朱',
|
||||
'高',
|
||||
'林',
|
||||
'何',
|
||||
'郭',
|
||||
'马',
|
||||
'罗',
|
||||
'梁',
|
||||
'宋',
|
||||
'郑',
|
||||
'谢',
|
||||
'韩',
|
||||
'唐',
|
||||
'冯',
|
||||
'于',
|
||||
'董',
|
||||
'萧',
|
||||
'程',
|
||||
'曹',
|
||||
'袁',
|
||||
'邓',
|
||||
'许',
|
||||
'傅',
|
||||
'沈',
|
||||
'曾',
|
||||
'彭',
|
||||
'吕',
|
||||
'苏',
|
||||
'卢',
|
||||
'蒋',
|
||||
'蔡',
|
||||
'贾',
|
||||
'丁',
|
||||
'魏',
|
||||
'薛',
|
||||
'叶',
|
||||
'阎',
|
||||
];
|
||||
|
||||
// 中文名字常用字
|
||||
const NAME_CHARS = [
|
||||
'伟',
|
||||
'芳',
|
||||
'娜',
|
||||
'秀英',
|
||||
'敏',
|
||||
'静',
|
||||
'丽',
|
||||
'强',
|
||||
'磊',
|
||||
'洋',
|
||||
'艳',
|
||||
'勇',
|
||||
'军',
|
||||
'杰',
|
||||
'娟',
|
||||
'涛',
|
||||
'明',
|
||||
'超',
|
||||
'秀兰',
|
||||
'霞',
|
||||
'平',
|
||||
'刚',
|
||||
'桂英',
|
||||
'文',
|
||||
'华',
|
||||
'飞',
|
||||
'鑫',
|
||||
'浩',
|
||||
'凯',
|
||||
'宁',
|
||||
'建',
|
||||
'峰',
|
||||
'辉',
|
||||
'成',
|
||||
'宇',
|
||||
'博',
|
||||
'泽',
|
||||
'思',
|
||||
'睿',
|
||||
'晨',
|
||||
'阳',
|
||||
'雪',
|
||||
'冰',
|
||||
'琳',
|
||||
'瑶',
|
||||
'婷',
|
||||
'欣',
|
||||
'悦',
|
||||
'佳',
|
||||
'慧',
|
||||
];
|
||||
|
||||
// 邮箱域名
|
||||
const EMAIL_DOMAINS = ['qq.com', '163.com', '126.com', 'gmail.com', 'outlook.com', 'hotmail.com'];
|
||||
|
||||
// 省份
|
||||
const PROVINCES = [
|
||||
'北京市',
|
||||
'天津市',
|
||||
'上海市',
|
||||
'重庆市',
|
||||
'河北省',
|
||||
'山西省',
|
||||
'辽宁省',
|
||||
'吉林省',
|
||||
'黑龙江省',
|
||||
'江苏省',
|
||||
'浙江省',
|
||||
'安徽省',
|
||||
'福建省',
|
||||
'江西省',
|
||||
'山东省',
|
||||
'河南省',
|
||||
'湖北省',
|
||||
'湖南省',
|
||||
'广东省',
|
||||
'海南省',
|
||||
'四川省',
|
||||
'贵州省',
|
||||
'云南省',
|
||||
'陕西省',
|
||||
'甘肃省',
|
||||
'青海省',
|
||||
'台湾省',
|
||||
'内蒙古自治区',
|
||||
'广西壮族自治区',
|
||||
'西藏自治区',
|
||||
'宁夏回族自治区',
|
||||
'新疆维吾尔自治区',
|
||||
];
|
||||
|
||||
// 城市
|
||||
const CITIES: Record<string, string[]> = {
|
||||
北京市: ['东城区', '西城区', '朝阳区', '海淀区'],
|
||||
上海市: ['黄浦区', '徐汇区', '长宁区', '静安区'],
|
||||
广东省: ['广州市', '深圳市', '东莞市', '佛山市'],
|
||||
浙江省: ['杭州市', '宁波市', '温州市', '嘉兴市'],
|
||||
江苏省: ['南京市', '苏州市', '无锡市', '常州市'],
|
||||
四川省: ['成都市', '绵阳市', '德阳市', '宜宾市'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机整数
|
||||
*/
|
||||
function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中随机选择
|
||||
*/
|
||||
function randomPick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文姓名生成器
|
||||
*/
|
||||
export const chineseName: GeneratorDefinition = {
|
||||
id: 'chineseName',
|
||||
name: '中文姓名',
|
||||
description: '生成随机中文姓名',
|
||||
categoryId: 'personal',
|
||||
params: [],
|
||||
generate: () => {
|
||||
const surname = randomPick(SURNAMES);
|
||||
const name = randomPick(NAME_CHARS);
|
||||
return surname + name;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 邮箱生成器
|
||||
*/
|
||||
export const email: GeneratorDefinition = {
|
||||
id: 'email',
|
||||
name: '邮箱',
|
||||
description: '生成随机邮箱地址',
|
||||
categoryId: 'personal',
|
||||
params: [
|
||||
{
|
||||
key: 'domain',
|
||||
label: '邮箱域名',
|
||||
type: 'select',
|
||||
defaultValue: 'random',
|
||||
description: '选择邮箱域名',
|
||||
options: [
|
||||
{ label: '随机', value: 'random' },
|
||||
...EMAIL_DOMAINS.map((d) => ({ label: d, value: d })),
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const domain =
|
||||
params.domain === 'random' ? randomPick(EMAIL_DOMAINS) : (params.domain as string);
|
||||
const username = Math.random().toString(36).substring(2, 10);
|
||||
return `${username}@${domain}`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 手机号生成器
|
||||
*/
|
||||
export const chinesePhone: GeneratorDefinition = {
|
||||
id: 'chinesePhone',
|
||||
name: '手机号',
|
||||
description: '生成随机中国手机号码',
|
||||
categoryId: 'personal',
|
||||
params: [
|
||||
{
|
||||
key: 'prefix',
|
||||
label: '号段前缀',
|
||||
type: 'select',
|
||||
defaultValue: 'random',
|
||||
description: '选择手机号前缀',
|
||||
options: [
|
||||
{ label: '随机', value: 'random' },
|
||||
{ label: '138', value: '138' },
|
||||
{ label: '139', value: '139' },
|
||||
{ label: '158', value: '158' },
|
||||
{ label: '188', value: '188' },
|
||||
{ label: '177', value: '177' },
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const prefixes = ['138', '139', '158', '188', '177', '136', '159', '186', '135', '150'];
|
||||
const prefix = params.prefix === 'random' ? randomPick(prefixes) : (params.prefix as string);
|
||||
const suffix = String(randomInt(10000000, 99999999));
|
||||
return prefix + suffix;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 身份证号生成器
|
||||
*/
|
||||
export const idCard: GeneratorDefinition = {
|
||||
id: 'idCard',
|
||||
name: '身份证号',
|
||||
description: '生成随机身份证号码',
|
||||
categoryId: 'personal',
|
||||
params: [
|
||||
{
|
||||
key: 'region',
|
||||
label: '地区',
|
||||
type: 'select',
|
||||
defaultValue: 'random',
|
||||
description: '选择身份证前6位地区码',
|
||||
options: [
|
||||
{ label: '随机', value: 'random' },
|
||||
{ label: '北京', value: '110101' },
|
||||
{ label: '上海', value: '310101' },
|
||||
{ label: '广州', value: '440103' },
|
||||
{ label: '深圳', value: '440305' },
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const regions = ['110101', '310101', '440103', '440305', '510104', '330102'];
|
||||
const region = params.region === 'random' ? randomPick(regions) : (params.region as string);
|
||||
|
||||
// 出生日期 1960-2005年
|
||||
const year = randomInt(1960, 2005);
|
||||
const month = String(randomInt(1, 12)).padStart(2, '0');
|
||||
const day = String(randomInt(1, 28)).padStart(2, '0');
|
||||
const birthday = `${year}${month}${day}`;
|
||||
|
||||
// 顺序码
|
||||
const sequence = String(randomInt(1, 999)).padStart(3, '0');
|
||||
|
||||
// 前17位
|
||||
const prefix17 = region + birthday + sequence;
|
||||
|
||||
// 校验码
|
||||
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 17; i++) {
|
||||
sum += parseInt(prefix17[i]) * weights[i];
|
||||
}
|
||||
const checkCode = checkCodes[sum % 11];
|
||||
|
||||
return prefix17 + checkCode;
|
||||
},
|
||||
generateAtIndex: (params, index) => {
|
||||
const regions = ['110101', '310101', '440103', '440305', '510104', '330102'];
|
||||
const region =
|
||||
params.region === 'random' ? regions[index % regions.length] : (params.region as string);
|
||||
|
||||
const year = 1990;
|
||||
const month = String(randomInt(1, 12)).padStart(2, '0');
|
||||
const day = String(randomInt(1, 28)).padStart(2, '0');
|
||||
const birthday = `${year}${month}${day}`;
|
||||
|
||||
const sequence = String(index + 1).padStart(3, '0');
|
||||
const prefix17 = region + birthday + sequence;
|
||||
|
||||
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 17; i++) {
|
||||
sum += parseInt(prefix17[i]) * weights[i];
|
||||
}
|
||||
const checkCode = checkCodes[sum % 11];
|
||||
|
||||
return prefix17 + checkCode;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 中文地址生成器
|
||||
*/
|
||||
export const chineseAddress: GeneratorDefinition = {
|
||||
id: 'chineseAddress',
|
||||
name: '中文地址',
|
||||
description: '生成随机中文地址',
|
||||
categoryId: 'personal',
|
||||
params: [
|
||||
{
|
||||
key: 'includeDetail',
|
||||
label: '包含详细地址',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
description: '是否包含街道门牌号',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const province = randomPick(PROVINCES);
|
||||
const cities = CITIES[province] || ['市区'];
|
||||
const city = randomPick(cities);
|
||||
const district = `区${randomInt(1, 20)}号`;
|
||||
|
||||
if (params.includeDetail) {
|
||||
const street = `路${randomInt(1, 200)}号`;
|
||||
return `${province}${city}${district}${street}`;
|
||||
}
|
||||
return `${province}${city}${district}`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 年龄生成器
|
||||
*/
|
||||
export const age: GeneratorDefinition = {
|
||||
id: 'age',
|
||||
name: '年龄',
|
||||
description: '生成随机年龄',
|
||||
categoryId: 'personal',
|
||||
params: [
|
||||
{
|
||||
key: 'min',
|
||||
label: '最小年龄',
|
||||
type: 'number',
|
||||
defaultValue: 18,
|
||||
min: 0,
|
||||
max: 150,
|
||||
description: '年龄最小值',
|
||||
},
|
||||
{
|
||||
key: 'max',
|
||||
label: '最大年龄',
|
||||
type: 'number',
|
||||
defaultValue: 65,
|
||||
min: 0,
|
||||
max: 150,
|
||||
description: '年龄最大值',
|
||||
},
|
||||
{
|
||||
key: 'distribution',
|
||||
label: '分布方式',
|
||||
type: 'select',
|
||||
defaultValue: 'uniform',
|
||||
description: '年龄分布方式',
|
||||
options: [
|
||||
{ label: '均匀分布', value: 'uniform' },
|
||||
{ label: '正态分布', value: 'normal' },
|
||||
{ label: '人口统计分布', value: 'demographic' },
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const min = (params.min as number) || 18;
|
||||
const max = (params.max as number) || 65;
|
||||
const distribution = (params.distribution as string) || 'uniform';
|
||||
|
||||
if (distribution === 'normal') {
|
||||
return String(normalRandom(min, max));
|
||||
} else if (distribution === 'demographic') {
|
||||
return String(demographicRandom());
|
||||
}
|
||||
return String(randomInt(min, max));
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 正态分布随机数
|
||||
*/
|
||||
function normalRandom(min: number, max: number): number {
|
||||
const mean = (min + max) / 2;
|
||||
const stdDev = (max - min) / 6;
|
||||
let u = 0,
|
||||
v = 0;
|
||||
while (u === 0) u = Math.random();
|
||||
while (v === 0) v = Math.random();
|
||||
const num = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
||||
const value = Math.round(mean + num * stdDev);
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 人口统计分布随机数(模拟真实年龄分布)
|
||||
*/
|
||||
function demographicRandom(): number {
|
||||
const ageGroups = [
|
||||
{ min: 0, max: 14, weight: 0.18 },
|
||||
{ min: 15, max: 24, weight: 0.12 },
|
||||
{ min: 25, max: 34, weight: 0.18 },
|
||||
{ min: 35, max: 44, weight: 0.17 },
|
||||
{ min: 45, max: 54, weight: 0.16 },
|
||||
{ min: 55, max: 64, weight: 0.12 },
|
||||
{ min: 65, max: 100, weight: 0.07 },
|
||||
];
|
||||
|
||||
const random = Math.random();
|
||||
let cumulative = 0;
|
||||
for (const group of ageGroups) {
|
||||
cumulative += group.weight;
|
||||
if (random <= cumulative) {
|
||||
return randomInt(group.min, group.max);
|
||||
}
|
||||
}
|
||||
return randomInt(25, 34);
|
||||
}
|
||||
|
||||
export const personalGenerators: GeneratorDefinition[] = [
|
||||
chineseName,
|
||||
email,
|
||||
chinesePhone,
|
||||
idCard,
|
||||
chineseAddress,
|
||||
age,
|
||||
];
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 技术数据生成器
|
||||
* 包含:UUID、IPv4、URL
|
||||
*/
|
||||
|
||||
import type { GeneratorDefinition } from '@/types/testDataGenerator';
|
||||
|
||||
/**
|
||||
* 生成随机整数
|
||||
*/
|
||||
function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中随机选择
|
||||
*/
|
||||
function randomPick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID 生成器
|
||||
*/
|
||||
export const uuid: GeneratorDefinition = {
|
||||
id: 'uuid',
|
||||
name: 'UUID',
|
||||
description: '生成随机 UUID',
|
||||
categoryId: 'technical',
|
||||
params: [
|
||||
{
|
||||
key: 'version',
|
||||
label: 'UUID 版本',
|
||||
type: 'select',
|
||||
defaultValue: 'v4',
|
||||
description: 'UUID 版本',
|
||||
options: [
|
||||
{ label: 'v4 (随机)', value: 'v4' },
|
||||
{ label: 'v1 (时间戳)', value: 'v1' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'uppercase',
|
||||
label: '大写字母',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
description: '是否使用大写字母',
|
||||
},
|
||||
{
|
||||
key: 'noDashes',
|
||||
label: '无连字符',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
description: '是否省略连字符',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const version = (params.version as string) || 'v4';
|
||||
const uppercase = params.uppercase === true;
|
||||
const noDashes = params.noDashes === true;
|
||||
|
||||
let result: string;
|
||||
if (version === 'v1') {
|
||||
result = generateUUIDv1();
|
||||
} else {
|
||||
result = generateUUIDv4();
|
||||
}
|
||||
|
||||
if (uppercase) {
|
||||
result = result.toUpperCase();
|
||||
}
|
||||
|
||||
if (noDashes) {
|
||||
result = result.replace(/-/g, '');
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成 UUID v4
|
||||
*/
|
||||
function generateUUIDv4(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 UUID v1(简化版本,模拟时间戳)
|
||||
*/
|
||||
function generateUUIDv1(): string {
|
||||
const now = Date.now();
|
||||
const timeLow = (now & 0xffffffff).toString(16).padStart(8, '0');
|
||||
const timeMid = ((now >> 32) & 0xffff).toString(16).padStart(4, '0');
|
||||
const timeHi = ((now >> 48) & 0x0fff) | 0x1000;
|
||||
const clockSeq = randomInt(0, 0x3fff) | 0x8000;
|
||||
const node = Array.from({ length: 6 }, () =>
|
||||
randomInt(0, 255).toString(16).padStart(2, '0'),
|
||||
).join('');
|
||||
|
||||
return `${timeLow}-${timeMid}-${timeHi.toString(16)}-${clockSeq.toString(16)}-${node}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* IPv4 生成器
|
||||
*/
|
||||
export const ipv4: GeneratorDefinition = {
|
||||
id: 'ipv4',
|
||||
name: 'IPv4',
|
||||
description: '生成随机 IPv4 地址',
|
||||
categoryId: 'technical',
|
||||
params: [
|
||||
{
|
||||
key: 'type',
|
||||
label: '地址类型',
|
||||
type: 'select',
|
||||
defaultValue: 'random',
|
||||
description: 'IP 地址类型',
|
||||
options: [
|
||||
{ label: '完全随机', value: 'random' },
|
||||
{ label: '内网地址', value: 'private' },
|
||||
{ label: '公网地址', value: 'public' },
|
||||
{ label: '环回地址', value: 'loopback' },
|
||||
],
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const type = (params.type as string) || 'random';
|
||||
|
||||
if (type === 'private') {
|
||||
return generatePrivateIPv4();
|
||||
} else if (type === 'loopback') {
|
||||
return '127.0.0.1';
|
||||
} else if (type === 'public') {
|
||||
return generatePublicIPv4();
|
||||
}
|
||||
return generateRandomIPv4();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成完全随机 IPv4
|
||||
*/
|
||||
function generateRandomIPv4(): string {
|
||||
return Array.from({ length: 4 }, () => randomInt(0, 255)).join('.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成内网 IPv4
|
||||
*/
|
||||
function generatePrivateIPv4(): string {
|
||||
const ranges = [
|
||||
{ prefix: '10', second: () => randomInt(0, 255) },
|
||||
{ prefix: '172', second: () => randomInt(16, 31) },
|
||||
{ prefix: '192.168', second: () => randomInt(0, 255) },
|
||||
];
|
||||
const range = randomPick(ranges);
|
||||
|
||||
if (range.prefix === '192.168') {
|
||||
return `192.168.${range.second()}.${randomInt(1, 254)}`;
|
||||
}
|
||||
return `${range.prefix}.${range.second()}.${randomInt(1, 254)}.${randomInt(1, 254)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成公网 IPv4
|
||||
*/
|
||||
function generatePublicIPv4(): string {
|
||||
let first: number;
|
||||
do {
|
||||
first = randomInt(1, 255);
|
||||
} while (first === 10 || first === 127 || first === 192 || first === 172);
|
||||
|
||||
return `${first}.${randomInt(0, 255)}.${randomInt(0, 255)}.${randomInt(1, 254)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 生成器
|
||||
*/
|
||||
export const url: GeneratorDefinition = {
|
||||
id: 'url',
|
||||
name: 'URL',
|
||||
description: '生成随机 URL',
|
||||
categoryId: 'technical',
|
||||
params: [
|
||||
{
|
||||
key: 'protocol',
|
||||
label: '协议',
|
||||
type: 'select',
|
||||
defaultValue: 'https',
|
||||
description: 'URL 协议',
|
||||
options: [
|
||||
{ label: 'HTTPS', value: 'https' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: '随机', value: 'random' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: 'URL 类型',
|
||||
type: 'select',
|
||||
defaultValue: 'website',
|
||||
description: 'URL 类型',
|
||||
options: [
|
||||
{ label: '网站', value: 'website' },
|
||||
{ label: 'API', value: 'api' },
|
||||
{ label: '图片', value: 'image' },
|
||||
{ label: '文件', value: 'file' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'includePath',
|
||||
label: '包含路径',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
description: '是否包含路径',
|
||||
},
|
||||
],
|
||||
generate: (params) => {
|
||||
const protocol = (params.protocol as string) || 'https';
|
||||
const type = (params.type as string) || 'website';
|
||||
const includePath = params.includePath !== false;
|
||||
|
||||
const actualProtocol = protocol === 'random' ? randomPick(['http', 'https']) : protocol;
|
||||
|
||||
const domains = [
|
||||
'example.com',
|
||||
'test.org',
|
||||
'demo.net',
|
||||
'sample.io',
|
||||
'api.service.com',
|
||||
'cdn.static.com',
|
||||
'img.media.com',
|
||||
];
|
||||
const domain = randomPick(domains);
|
||||
|
||||
let path = '';
|
||||
if (includePath) {
|
||||
if (type === 'api') {
|
||||
path = `/api/v${randomInt(1, 3)}/${randomPick(['users', 'products', 'orders', 'items'])}`;
|
||||
} else if (type === 'image') {
|
||||
path = `/images/${randomPick(['avatar', 'banner', 'logo', 'photo'])}/${randomInt(1, 1000)}.jpg`;
|
||||
} else if (type === 'file') {
|
||||
path = `/files/${randomPick(['document', 'report', 'data'])}/${randomInt(1, 100)}.pdf`;
|
||||
} else {
|
||||
path = `/${randomPick(['about', 'contact', 'products', 'services', 'blog'])}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加查询参数
|
||||
let query = '';
|
||||
if (Math.random() > 0.5) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('id', String(randomInt(1, 10000)));
|
||||
if (Math.random() > 0.5) params.set('page', String(randomInt(1, 100)));
|
||||
if (Math.random() > 0.7) params.set('lang', randomPick(['zh', 'en', 'ja']));
|
||||
query = `?${params.toString()}`;
|
||||
}
|
||||
|
||||
return `${actualProtocol}://${domain}${path}${query}`;
|
||||
},
|
||||
};
|
||||
|
||||
export const technicalGenerators: GeneratorDefinition[] = [uuid, ipv4, url];
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 生成器内部类型定义
|
||||
* 与 testDataGenerator.ts 分离,用于生成器库内部
|
||||
*/
|
||||
|
||||
import type { GeneratorDefinition, GeneratorCategory } from '@/types/testDataGenerator';
|
||||
|
||||
export type { GeneratorDefinition, GeneratorCategory };
|
||||
|
||||
/**
|
||||
* 生成器注册表
|
||||
*/
|
||||
export interface GeneratorRegistry {
|
||||
/** 分类列表 */
|
||||
categories: GeneratorCategory[];
|
||||
/** 生成器列表 */
|
||||
generators: GeneratorDefinition[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机数生成选项
|
||||
*/
|
||||
export interface RandomOptions {
|
||||
/** 最小值 */
|
||||
min?: number;
|
||||
/** 最大值 */
|
||||
max?: number;
|
||||
/** 是否包含最小值 */
|
||||
includeMin?: boolean;
|
||||
/** 是否包含最大值 */
|
||||
includeMax?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符集选项
|
||||
*/
|
||||
export interface CharsetOptions {
|
||||
/** 是否包含大写字母 */
|
||||
uppercase?: boolean;
|
||||
/** 是否包含小写字母 */
|
||||
lowercase?: boolean;
|
||||
/** 是否包含数字 */
|
||||
digits?: boolean;
|
||||
/** 是否包含特殊字符 */
|
||||
special?: boolean;
|
||||
/** 自定义字符集 */
|
||||
custom?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user