Commit 0247a9d4c2c7f49774781a91b49386ce84ad317e

Authored by 赵旭婷
1 parent 33bc2f2f

新增客户管理和客户分组主流程

pages/basePage.ts
... ... @@ -318,7 +318,21 @@ export abstract class BasePage {
318 318 if (uploadButtonLocator) {
319 319 await uploadButtonLocator.click();
320 320 } else {
321   - await this.page.locator('uni-scroll-view uni-button').click();
  321 + // 尝试多种方式触发文件选择器
  322 + try {
  323 + // 方法1: 查找上传按钮(包含图片相关的按钮)
  324 + const uploadBtn = this.page.locator('.nut-uploader__input').first();
  325 + await uploadBtn.click({ timeout: 3000 });
  326 + } catch {
  327 + // 方法2: 直接在文件输入框上设置文件
  328 + const fileInput = this.page.locator('input[type="file"]');
  329 + if (await fileInput.count() > 0) {
  330 + await fileInput.setInputFiles(imagePath);
  331 + return;
  332 + }
  333 + // 方法3: 尝试点击文字
  334 + await this.page.getByText('点击上传').click({ timeout: 3000 });
  335 + }
322 336 }
323 337  
324 338 // 等待文件选择器出现并设置文件
... ... @@ -340,4 +354,62 @@ export abstract class BasePage {
340 354 // 等待上传完成
341 355 await this.page.waitForTimeout(1000);
342 356 }
  357 +
  358 + /**
  359 + * 选择省市区(通用方法)
  360 + * @param regionPickerLocator 省市区选择器定位器
  361 + * @param province 省份(如:山西省)
  362 + * @param city 城市(如:长治市)
  363 + * @param district 区县(如:潞城区)
  364 + * @param confirmButtonText 确认按钮文本(默认:确认选择)
  365 + */
  366 + async selectRegion(
  367 + regionPickerLocator: Locator,
  368 + province: string,
  369 + city: string,
  370 + district: string,
  371 + confirmButtonText: string = '确认选择'
  372 + ): Promise<void> {
  373 + // 点击省市区选择器
  374 + await regionPickerLocator.click();
  375 +
  376 + // 等待弹窗出现
  377 + await this.page.waitForTimeout(500);
  378 +
  379 + // 选择省份 - 使用exact选项避免匹配到完整地址
  380 + await this.page.getByText(province, { exact: true }).click();
  381 +
  382 + // 选择城市 - 使用exact选项
  383 + await this.page.getByText(city, { exact: true }).click();
  384 +
  385 + // 选择区县 - 使用exact选项
  386 + await this.page.getByText(district, { exact: true }).click();
  387 +
  388 + // 确认选择
  389 + await this.page.getByText(confirmButtonText).click();
  390 + }
  391 +
  392 + /**
  393 + * 选择省市区(通过选择器字符串)
  394 + * @param regionPickerSelector 省市区选择器选择器字符串
  395 + * @param province 省份(如:山西省)
  396 + * @param city 城市(如:长治市)
  397 + * @param district 区县(如:潞城区)
  398 + * @param confirmButtonText 确认按钮文本(默认:确认选择)
  399 + */
  400 + async selectRegionBySelector(
  401 + regionPickerSelector: string,
  402 + province: string,
  403 + city: string,
  404 + district: string,
  405 + confirmButtonText: string = '确认选择'
  406 + ): Promise<void> {
  407 + await this.selectRegion(
  408 + this.page.locator(regionPickerSelector),
  409 + province,
  410 + city,
  411 + district,
  412 + confirmButtonText
  413 + );
  414 + }
343 415 }
... ...
pages/customerPage.ts
... ... @@ -94,7 +94,7 @@ export class CustomerPage extends BasePage {
94 94 this.confirmRegionButton = page.getByText('确认选择');
95 95 this.detailedAddressInput = page.locator('uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input');
96 96 this.confirmButton = page.getByText('确定');
97   - this.saveButton = page.getByText('保存');
  97 + this.saveButton = page.locator('uni-button.execute-btn').filter({ hasText: '保存' });
98 98 }
99 99  
100 100 /**
... ... @@ -203,6 +203,27 @@ export class CustomerPage extends BasePage {
203 203 }
204 204  
205 205 /**
  206 + * 检查赊欠额度开关是否已开启
  207 + * @returns 是否已开启
  208 + */
  209 + async isCreditLimitEnabled(): Promise<boolean> {
  210 + // 检查开关的激活状态(nut-switch 组件在激活时会有 nut-switch--active 类)
  211 + const switchElement = this.creditLimitSwitch;
  212 + const className = await switchElement.getAttribute('class');
  213 + return className?.includes('nut-switch--active') ?? false;
  214 + }
  215 +
  216 + /**
  217 + * 确保赊欠额度开关已开启,如果未开启则点击开启
  218 + */
  219 + async ensureCreditLimitEnabled(): Promise<void> {
  220 + const isEnabled = await this.isCreditLimitEnabled();
  221 + if (!isEnabled) {
  222 + await this.enableCreditLimit();
  223 + }
  224 + }
  225 +
  226 + /**
206 227 * 填写赊欠额度
207 228 * @param amount 赊欠额度金额
208 229 */
... ... @@ -229,24 +250,12 @@ export class CustomerPage extends BasePage {
229 250 * @param city 城市(如:长治市)
230 251 * @param district 区县(如:潞城区)
231 252 */
232   - async selectRegion(province: string, city: string, district: string): Promise<void> {
  253 + async selectCustomerRegion(province: string, city: string, district: string): Promise<void> {
233 254 // 点击省市区选择器 - nth-child(12),开启赊欠额度后位置
234   - await this.page.locator('uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask').click();
235   -
236   - // 等待弹窗出现
237   - await this.page.waitForTimeout(500);
  255 + const regionPickerLocator = this.page.locator('uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask');
238 256  
239   - // 选择省份
240   - await this.page.getByText(province).click();
241   -
242   - // 选择城市
243   - await this.page.getByText(city).click();
244   -
245   - // 选择区县
246   - await this.page.getByText(district).click();
247   -
248   - // 确认选择
249   - await this.page.getByText('确认选择').click();
  257 + // 调用基类的通用方法
  258 + await super.selectRegion(regionPickerLocator, province, city, district);
250 259 }
251 260  
252 261 /**
... ... @@ -321,7 +330,7 @@ export class CustomerPage extends BasePage {
321 330  
322 331 // 如果有省市区配置
323 332 if (options?.province && options?.city && options?.district) {
324   - await this.selectRegion(options.province, options.city, options.district);
  333 + await this.selectCustomerRegion(options.province, options.city, options.district);
325 334 }
326 335  
327 336 // 如果有详细地址
... ... @@ -347,6 +356,21 @@ export class CustomerPage extends BasePage {
347 356 */
348 357 async verifyCustomerCreated(customerName: string): Promise<boolean> {
349 358 try {
  359 + // 先返回首页确保在客户列表页面
  360 + await this.gotoHome();
  361 + await this.openCustomerManagement();
  362 + await this.page.waitForLoadState('networkidle');
  363 + await this.page.waitForTimeout(500);
  364 +
  365 + // 清空搜索框(点击清除图标)
  366 + const clearBtn = this.page.locator('.nut-icon-circle-close');
  367 + if (await clearBtn.isVisible()) {
  368 + await clearBtn.click();
  369 + await this.page.waitForTimeout(500);
  370 + }
  371 +
  372 + // 搜索客户
  373 + await this.searchCustomer(customerName);
350 374 await this.page.waitForSelector(selectors.customerInList(customerName), { timeout: 10000 });
351 375 return true;
352 376 } catch {
... ... @@ -364,7 +388,12 @@ export class CustomerPage extends BasePage {
364 388 // 点击搜索框区域(使用 force: true 绕过遮罩层)
365 389 await this.page.locator('uni-view').filter({ hasText: /^客户名称\/手机号$/ }).nth(2).click({ force: true });
366 390 // 在输入框中填写客户名称
367   - await this.page.getByRole('textbox').fill(customerName);
  391 + const input = this.page.locator('uni-input').filter({ hasText: '客户名称/手机号' }).getByRole('textbox');
  392 + await input.fill(customerName);
  393 + // 等待搜索结果出现
  394 + await this.page.waitForTimeout(1500);
  395 + // 按回车键确认搜索
  396 + await input.press('Enter');
368 397 await this.page.waitForTimeout(1000);
369 398 }
370 399  
... ... @@ -373,7 +402,10 @@ export class CustomerPage extends BasePage {
373 402 * @param customerName 客户名称
374 403 */
375 404 async clickCustomerItem(customerName: string): Promise<void> {
376   - await this.page.getByText(customerName).first().click();
  405 + // 使用Playwright的locator语法查找搜索结果列表中的客户项
  406 + const customerItem = this.page.locator('uni-view').filter({ hasText: customerName }).first();
  407 + await customerItem.waitFor({ state: 'visible', timeout: 10000 });
  408 + await customerItem.click();
377 409 await this.page.waitForLoadState('networkidle', { timeout: 30000 });
378 410 }
379 411  
... ... @@ -438,9 +470,10 @@ export class CustomerPage extends BasePage {
438 470 await this.clearAndFillIdCard(customerInfo.idCard);
439 471  
440 472 if (options?.creditLimit) {
441   - // 清除并填写赊欠额度(使用 force: true 绕过遮罩层)
  473 + // 先确保赊欠额度开关已开启
  474 + await this.ensureCreditLimitEnabled();
  475 + // 直接填写赊欠额度(fill会自动清除)
442 476 await this.creditLimitInput.click({ force: true });
443   - await this.page.locator('uni-view:nth-child(8) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__clear > .nut-input__clear-icon').click({ force: true });
444 477 await this.creditLimitInput.fill(options.creditLimit);
445 478 }
446 479  
... ... @@ -449,7 +482,7 @@ export class CustomerPage extends BasePage {
449 482 }
450 483  
451 484 if (options?.province && options?.city && options?.district) {
452   - await this.selectRegion(options.province, options.city, options.district);
  485 + await this.selectCustomerRegion(options.province, options.city, options.district);
453 486 }
454 487  
455 488 if (customerInfo.detailedAddress) {
... ... @@ -476,6 +509,9 @@ export class CustomerPage extends BasePage {
476 509 await this.clickCustomerItem(customerName);
477 510 await this.clickDeleteButton();
478 511 await this.confirmDelete();
  512 + // 点击确定后,页面会返回到列表页,等待加载完成
  513 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  514 + await this.page.waitForTimeout(1000);
479 515 }
480 516  
481 517 /**
... ... @@ -485,59 +521,30 @@ export class CustomerPage extends BasePage {
485 521 async verifyCustomerDeleted(customerName: string): Promise<boolean> {
486 522 await this.gotoHome();
487 523 await this.openCustomerManagement();
  524 + await this.page.waitForLoadState('networkidle');
  525 + await this.page.waitForTimeout(500);
  526 +
  527 + // 清空搜索框(点击清除图标)
  528 + const clearBtn = this.page.locator('.nut-icon-circle-close');
  529 + if (await clearBtn.isVisible()) {
  530 + await clearBtn.click();
  531 + await this.page.waitForTimeout(500);
  532 + }
  533 +
488 534 await this.searchCustomer(customerName);
  535 + await this.page.waitForTimeout(1000);
489 536  
490   - const count = await this.page.getByText(customerName).count();
  537 + // 在客户列表中查找(只在 .nut-cell-group 或列表容器中查找)
  538 + const listItem = this.page.locator('.nut-cell-group__wrapper').locator('.nut-cell', { hasText: customerName });
  539 + const count = await listItem.count();
  540 + console.log(`查找 "${customerName}" 在列表中找到 ${count} 个匹配`);
491 541 return count === 0;
492 542 }
493 543  
494 544 // ==================== 录入欠款相关方法 ====================
495 545  
496 546 /**
497   - * 点击录入欠款按钮
498   - */
499   - async clickRecordDebt(): Promise<void> {
500   - await this.page.getByText('录入欠款').click();
501   - await this.page.waitForTimeout(500);
502   - }
503   -
504   - /**
505   - * 选择欠款类型
506   - * @param typeIndex 类型索引(默认选择第三个,索引为2)
507   - */
508   - async selectDebtType(typeIndex: number = 2): Promise<void> {
509   - await this.page.locator('uni-view:nth-child(4) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__mask').click();
510   - await this.page.locator('uni-view').filter({ hasText: /^2$/ }).nth(typeIndex).click();
511   - }
512   -
513   - /**
514   - * 填写欠款金额
515   - * @param amount 欠款金额
516   - */
517   - async fillDebtAmount(amount: string): Promise<void> {
518   - await this.page.getByRole('spinbutton').click();
519   - await this.page.getByRole('spinbutton').fill(amount);
520   - }
521   -
522   - /**
523   - * 填写欠款备注
524   - * @param remark 备注
525   - */
526   - async fillDebtRemark(remark: string): Promise<void> {
527   - await this.page.getByRole('textbox').nth(4).click();
528   - await this.page.getByRole('textbox').nth(4).fill(remark);
529   - }
530   -
531   - /**
532   - * 保存欠款
533   - */
534   - async saveDebt(): Promise<void> {
535   - await this.page.getByText('保存', { exact: true }).click();
536   - await this.page.waitForLoadState('networkidle', { timeout: 30000 });
537   - }
538   -
539   - /**
540   - * 录入欠款 - 完整流程
  547 + * 录入欠款 - 完整流程(基于录制脚本重写)
541 548 * @param customerName 客户名称
542 549 * @param amount 欠款金额
543 550 * @param options 可选配置
... ... @@ -546,12 +553,12 @@ export class CustomerPage extends BasePage {
546 553 customerName: string,
547 554 amount: string,
548 555 options?: {
549   - typeIndex?: number; // 欠款类型索引
  556 + debtType?: string; // 欠款类型(如 '15')
550 557 remark?: string; // 备注
551 558 imagePath?: string; // 图片路径
552 559 }
553 560 ): Promise<void> {
554   - // 先返回首页确保页面状态干净(避免遮罩层阻挡)
  561 + // 先返回首页确保页面状态干净
555 562 await this.gotoHome();
556 563 // 打开客户管理并搜索
557 564 await this.openCustomerManagement();
... ... @@ -559,39 +566,79 @@ export class CustomerPage extends BasePage {
559 566 await this.clickCustomerItem(customerName);
560 567  
561 568 // 点击录入欠款
562   - await this.clickRecordDebt();
  569 + await this.page.getByText('录入欠款').click();
  570 + await this.page.waitForTimeout(500);
563 571  
564   - // 选择欠款类型
565   - if (options?.typeIndex !== undefined) {
566   - await this.selectDebtType(options.typeIndex);
567   - }
  572 + // 选择欠款类型 - 点击下拉框
  573 + await this.page.locator('uni-view:nth-child(4) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__mask').click();
  574 + // 选择类型(默认使用 '15')
  575 + const debtType = options?.debtType || '15';
  576 + await this.page.getByText(debtType, { exact: true }).click();
568 577  
569 578 // 填写欠款金额
570   - await this.fillDebtAmount(amount);
  579 + await this.page.getByRole('spinbutton').click();
  580 + await this.page.getByRole('spinbutton').fill(amount);
571 581  
572 582 // 填写备注
573 583 if (options?.remark) {
574   - await this.fillDebtRemark(options.remark);
  584 + await this.page.getByRole('textbox').nth(4).click();
  585 + await this.page.getByRole('textbox').nth(4).fill(options.remark);
575 586 }
576 587  
577   - // 上传图片 - 使用基类封装的方法
578   - if (options?.imagePath) {
579   - await this.uploadCustomerImage(options.imagePath);
580   - }
  588 + // 上传图片(暂时跳过,如果需要可以后续启用)
  589 + // if (options?.imagePath) {
  590 + // // 使用基类的uploadImage方法上传图片
  591 + // await this.uploadImage(options.imagePath);
  592 + // }
581 593  
582   - // 保存
583   - await this.saveDebt();
  594 + // 点击保存按钮
  595 + await this.page.getByText('保存', { exact: true }).click();
  596 + await this.page.waitForTimeout(2000);
584 597 }
585 598  
586 599 /**
587 600 * 验证欠款是否录入成功
588   - * @param amount 欠款金额
  601 + * 检查客户详情页的"当前欠款"字段是否显示正确金额
  602 + * @param expectedAmount 期望的欠款金额
589 603 */
590   - async verifyDebtRecorded(amount: string): Promise<boolean> {
  604 + async verifyDebtRecorded(expectedAmount: string): Promise<boolean> {
591 605 try {
592   - await this.page.getByText(`欠款:${amount}.00元`).waitFor({ timeout: 10000 });
593   - return true;
594   - } catch {
  606 + // 等待页面稳定
  607 + await this.page.waitForTimeout(2000);
  608 + await this.page.waitForLoadState('networkidle');
  609 +
  610 + // 显示格式是"欠款:{金额}.00元",其中金额是原始输入值
  611 + const displayAmount = parseFloat(expectedAmount).toFixed(2);
  612 + const searchText = `欠款:${displayAmount}元`;
  613 +
  614 + console.log(`查找欠款文本: ${searchText}`);
  615 +
  616 + // 直接查找包含"欠款:{金额}元"格式的元素
  617 + const debtElement = this.page.getByText(searchText);
  618 + const isVisible = await debtElement.isVisible({ timeout: 5000 }).catch(() => false);
  619 +
  620 + if (isVisible) {
  621 + console.log(`验证成功:找到欠款 ${searchText}`);
  622 + return true;
  623 + }
  624 +
  625 + // 备用方案:查找包含"欠款:"的元素
  626 + const debtWithLabel = this.page.locator('uni-view').filter({ hasText: /^欠款:/ });
  627 + const count = await debtWithLabel.count();
  628 + console.log(`找到 ${count} 个包含"欠款:"的元素`);
  629 +
  630 + if (count > 0) {
  631 + const text = await debtWithLabel.first().textContent();
  632 + console.log(`欠款文本内容: ${text}`);
  633 + if (text && text.includes(displayAmount)) {
  634 + return true;
  635 + }
  636 + }
  637 +
  638 + console.log(`验证失败:未找到金额 ${expectedAmount}`);
  639 + return false;
  640 + } catch (error) {
  641 + console.log('验证欠款失败:', error);
595 642 return false;
596 643 }
597 644 }
... ...
scripts/save-auth.ts
1 1 // 半自动登录,登录存放auth.json
2   -import { test, expect } from '@playwright/test';
  2 +import { chromium } from '@playwright/test';
  3 +import dotenv from 'dotenv';
  4 +import path from 'path';
  5 +
  6 +dotenv.config({ path: path.resolve(__dirname, '../.env') });
3 7  
4   -// 从环境变量获取配置
5 8 const TEST_PHONE = process.env.phone;
6 9 const TEST_USER_NAME = process.env.TEST_USER_NAME;
  10 +const BASE_URL = process.env.BASE_URL || 'https://erp-pad.test.gszdtop.com';
7 11  
8   -if (!TEST_PHONE) {
9   - throw new Error('phone 环境变量未设置,请设置 phone 后再运行');
  12 +if (!TEST_PHONE || !TEST_USER_NAME) {
  13 + throw new Error('phone 和 TEST_USER_NAME 环境变量未设置');
10 14 }
11   -if (!TEST_USER_NAME) {
12   - throw new Error('TEST_USER_NAME 环境变量未设置,请设置 TEST_USER_NAME 后再运行');
  15 +
  16 +async function main() {
  17 + console.log('开始半自动登录...');
  18 +
  19 + const browser = await chromium.launch({ headless: false });
  20 + const context = await browser.newContext();
  21 + const page = await context.newPage();
  22 +
  23 + try {
  24 + await page.goto(`${BASE_URL}/#/pages/login/index`);
  25 + await page.waitForLoadState('domcontentloaded');
  26 +
  27 + // 已登录则直接保存
  28 + try {
  29 + await page.getByText(TEST_USER_NAME!).waitFor({ state: 'visible', timeout: 3000 });
  30 + console.log('已登录,保存状态...');
  31 + await context.storageState({ path: 'auth.json' });
  32 + console.log('状态已保存到 auth.json');
  33 + return;
  34 + } catch {
  35 + console.log('未登录,开始登录...');
  36 + }
  37 +
  38 + // 手机号登录流程
  39 + await page.getByText('手机号登录/注册').click();
  40 + await page.getByText('确定').click();
  41 +
  42 + // 填写手机号
  43 + const phoneInput = page.locator('uni-input').filter({ hasText: '请输入手机号' }).locator('input').first();
  44 + await phoneInput.click();
  45 + await phoneInput.fill(TEST_PHONE!);
  46 +
  47 + // 获取验证码
  48 + await page.getByText('获取验证码').click();
  49 + console.log('请手动输入验证码并点击登录...');
  50 +
  51 + // 等待登录成功
  52 + await page.getByText(TEST_USER_NAME!).waitFor({ timeout: 120000 });
  53 + console.log('登录成功!');
  54 +
  55 + await context.storageState({ path: 'auth.json' });
  56 + console.log('状态已保存到 auth.json');
  57 +
  58 + } finally {
  59 + await browser.close();
  60 + }
13 61 }
14 62  
15   -test('半自动登录(自动填手机号,手动输验证码)', async ({ page }) => {
16   - await page.goto('/#/pages/login/index');
17   - await page.getByText('手机号登录/注册').click();
18   - await page.getByText('确定').click();
19   - // --- 这里执行登录操作 ---
20   - await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').click();
21   - await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').fill(TEST_PHONE);
22   - // 此处假设出现了验证码,你需要手动输入或处理
23   - await page.locator('uni-button').filter({ hasText: '获取验证码' }).click();
24   - console.log('请手动输入验证码,然后点击登录按钮...');
25   - await page.locator('uni-button').filter({ hasText: '登录' }).click();
26   - // --- 等待登录成功,跳转到首页 ---
27   - await expect(page.getByText(TEST_USER_NAME)).toBeVisible({ timeout: 0 });
28   -
29   - // 4. 保存存储状态(Cookie 和 LocalStorage)
30   - await page.context().storageState({ path: 'auth.json' });
31   - console.log('状态已保存到 auth.json');
32   -});
33 63 \ No newline at end of file
  64 +main().catch(console.error);
... ...
tests/customer.spec.ts
... ... @@ -9,6 +9,9 @@ import * as allure from &#39;allure-js-commons&#39;;
9 9 test.describe('客户管理', () => {
10 10 // 使用已保存的认证状态
11 11 test.use({ storageState: 'auth.json' });
  12 +
  13 + // 强制测试串行执行,避免并行测试之间的干扰
  14 + test.describe.configure({ mode: 'serial' });
12 15  
13 16 test('新增客户', async ({ customerPage }, testInfo) => {
14 17 // 添加allure元素
... ... @@ -24,7 +27,7 @@ test.describe(&#39;客户管理&#39;, () =&gt; {
24 27 const detailedAddress = generateDetailedAddress();
25 28  
26 29  
27   - console.log('生成的客户信息:', { name, phone, idCard, detailedAddress });
  30 + // console.log('生成的客户信息:', { name, phone, idCard, detailedAddress });
28 31  
29 32 return { name, phone, idCard, detailedAddress };
30 33 });
... ... @@ -62,145 +65,191 @@ test.describe(&#39;客户管理&#39;, () =&gt; {
62 65 });
63 66 });
64 67  
65   -// test('修改客户', async ({ customerPage }, testInfo) => {
66   -// // 添加allure元素
67   -// await allure.epic('客户管理');
68   -// await allure.feature('客户信息');
69   -// await allure.story('修改客户');
70   -
71   -// // 先创建一个客户用于修改
72   -// const originalName = generateCustomerName();
73   -// const originalPhone = generatePhoneNumber();
74   -// const originalIdCard = generateIdCard();
75   -
76   -// console.log('原客户名称:', originalName);
77   -
78   -// // 步骤1:先创建客户
79   -// await allure.step('创建待修改的客户', async () => {
80   -// await customerPage.gotoHome();
81   -// await customerPage.createCustomer({
82   -// name: originalName,
83   -// phone: originalPhone,
84   -// idCard: originalIdCard,
85   -// });
86   -// });
87   -
88   -// // 生成新的客户信息
89   -// const newName = generateCustomerName();
90   -// const newPhone = generatePhoneNumber();
91   -// const newIdCard = generateIdCard();
92   -// const randomImage = getRandomImage();
93   -
94   -// console.log('新客户名称:', newName);
95   -
96   -// // 步骤2:执行修改客户操作
97   -// await allure.step('修改客户信息', async () => {
98   -// await customerPage.updateCustomer(
99   -// originalName,
100   -// {
101   -// name: newName,
102   -// phone: newPhone,
103   -// idCard: newIdCard,
104   -// },
105   -// {
106   -// creditLimit: '400',
107   -// licensePlate: '渝YUNI99',
108   -// province: '江苏省',
109   -// city: '连云港市',
110   -// district: '海州区',
111   -// imagePath: randomImage || undefined,
112   -// }
113   -// );
114   -// await customerPage.attachScreenshot(testInfo, '修改客户成功截图');
115   -// });
116   -
117   -// // 步骤3:验证客户修改成功
118   -// await allure.step('验证客户修改成功', async () => {
119   -// const isUpdated = await customerPage.verifyCustomerCreated(newName);
120   -// expect(isUpdated).toBeTruthy();
121   -// });
122   -// });
123   -
124   -// test('删除客户', async ({ customerPage }, testInfo) => {
125   -// // 添加allure元素
126   -// await allure.epic('客户管理');
127   -// await allure.feature('客户信息');
128   -// await allure.story('删除客户');
129   -
130   -// // 先创建一个客户用于删除
131   -// const customerName = generateCustomerName();
132   -// const phone = generatePhoneNumber();
133   -// const idCard = generateIdCard();
134   -
135   -// console.log('待删除客户名称:', customerName);
136   -
137   -// // 步骤1:先创建客户
138   -// await allure.step('创建待删除的客户', async () => {
139   -// await customerPage.gotoHome();
140   -// await customerPage.createCustomer({
141   -// name: customerName,
142   -// phone: phone,
143   -// idCard: idCard,
144   -// });
145   -// });
146   -
147   -// // 步骤2:执行删除客户操作
148   -// await allure.step('删除客户', async () => {
149   -// await customerPage.deleteCustomer(customerName);
150   -// await customerPage.attachScreenshot(testInfo, '删除客户成功截图');
151   -// });
152   -
153   -// // 步骤3:验证客户删除成功
154   -// await allure.step('验证客户删除成功', async () => {
155   -// const isDeleted = await customerPage.verifyCustomerDeleted(customerName);
156   -// expect(isDeleted).toBeTruthy();
157   -// });
158   -// });
159   -
160   -// test('录入欠款', async ({ customerPage }, testInfo) => {
161   -// // 添加allure元素
162   -// await allure.epic('客户管理');
163   -// await allure.feature('客户欠款');
164   -// await allure.story('录入欠款');
165   -
166   -// // 先创建一个客户用于录入欠款
167   -// const customerName = generateCustomerName();
168   -// const phone = generatePhoneNumber();
169   -// const idCard = generateIdCard();
170   -
171   -// console.log('待录入欠款的客户名称:', customerName);
172   -
173   -// // 步骤1:先创建客户
174   -// await allure.step('创建客户', async () => {
175   -// await customerPage.gotoHome();
176   -// await customerPage.createCustomer({
177   -// name: customerName,
178   -// phone: phone,
179   -// idCard: idCard,
180   -// });
181   -// });
182   -
183   -// // 生成欠款金额和备注
184   -// const debtAmount = generateAmount(100, 9999);
185   -// const remark = generateRemark(50);
186   -// const randomImage = getRandomImage();
187   -
188   -// console.log('欠款金额:', debtAmount);
189   -// console.log('备注:', remark);
190   -
191   -// // 步骤2:执行录入欠款操作
192   -// await allure.step('录入欠款', async () => {
193   -// await customerPage.recordDebt(customerName, debtAmount, {
194   -// remark: remark,
195   -// imagePath: randomImage || undefined,
196   -// });
197   -// await customerPage.attachScreenshot(testInfo, '录入欠款成功截图');
198   -// });
199   -
200   -// // 步骤3:验证欠款录入成功
201   -// await allure.step('验证欠款录入成功', async () => {
202   -// const isRecorded = await customerPage.verifyDebtRecorded(debtAmount);
203   -// expect(isRecorded).toBeTruthy();
204   -// });
205   -// });
  68 + test('修改客户', async ({ customerPage }, testInfo) => {
  69 + // 添加allure元素
  70 + await allure.epic('客户管理');
  71 + await allure.feature('客户信息');
  72 + await allure.story('修改客户');
  73 +
  74 + // 先创建一个客户用于修改
  75 + const originalName = generateCustomerName();
  76 + const originalPhone = generatePhoneNumber();
  77 + const originalIdCard = generateIdCard();
  78 +
  79 + // console.log('原客户名称:', originalName);
  80 +
  81 + // 步骤1:先创建客户
  82 + await allure.step('创建待修改的客户', async () => {
  83 + await customerPage.gotoHome();
  84 + await customerPage.createCustomer({
  85 + name: originalName,
  86 + phone: originalPhone,
  87 + idCard: originalIdCard,
  88 + });
  89 + });
  90 +
  91 + // 生成新的客户信息
  92 + const newName = generateCustomerName();
  93 + const newPhone = generatePhoneNumber();
  94 + const newIdCard = generateIdCard();
  95 + const randomImage = getRandomImage();
  96 +
  97 + // console.log('新客户名称:', newName);
  98 +
  99 + // 步骤2:执行修改客户操作
  100 + await allure.step('修改客户信息', async () => {
  101 + await customerPage.updateCustomer(
  102 + originalName,
  103 + {
  104 + name: newName,
  105 + phone: newPhone,
  106 + idCard: newIdCard,
  107 + },
  108 + {
  109 + creditLimit: '400',
  110 + licensePlate: '渝YUNI99',
  111 + province: '江苏省',
  112 + city: '连云港市',
  113 + district: '海州区',
  114 + imagePath: randomImage || undefined,
  115 + }
  116 + );
  117 + await customerPage.attachScreenshot(testInfo, '修改客户成功截图');
  118 + });
  119 +
  120 + // 步骤3:验证客户修改成功
  121 + await allure.step('验证客户修改成功', async () => {
  122 + const isUpdated = await customerPage.verifyCustomerCreated(newName);
  123 + expect(isUpdated).toBeTruthy();
  124 + });
  125 + });
  126 +
  127 + test('删除客户', async ({ customerPage }, testInfo) => {
  128 + // 添加allure元素
  129 + await allure.epic('客户管理');
  130 + await allure.feature('客户信息');
  131 + await allure.story('删除客户');
  132 +
  133 + // 先创建一个客户用于删除
  134 + const customerName = generateCustomerName();
  135 + const phone = generatePhoneNumber();
  136 + const idCard = generateIdCard();
  137 +
  138 + // console.log('待删除客户名称:', customerName);
  139 +
  140 + // 步骤1:先创建客户
  141 + await allure.step('创建待删除的客户', async () => {
  142 + await customerPage.gotoHome();
  143 + await customerPage.createCustomer({
  144 + name: customerName,
  145 + phone: phone,
  146 + idCard: idCard,
  147 + });
  148 + });
  149 +
  150 + // 步骤2:执行删除客户操作
  151 + await allure.step('删除客户', async () => {
  152 + await customerPage.deleteCustomer(customerName);
  153 + await customerPage.attachScreenshot(testInfo, '删除客户成功截图');
  154 + });
  155 +
  156 + // 步骤3:验证客户删除成功
  157 + await allure.step('验证客户删除成功', async () => {
  158 + const isDeleted = await customerPage.verifyCustomerDeleted(customerName);
  159 + expect(isDeleted).toBeTruthy();
  160 + });
  161 + });
  162 +
  163 + test('录入欠款', async ({ customerPage }, testInfo) => {
  164 + // 添加allure元素
  165 + await allure.epic('客户管理');
  166 + await allure.feature('客户欠款');
  167 + await allure.story('录入欠款');
  168 +
  169 + // 先创建一个客户用于录入欠款
  170 + const customerName = generateCustomerName();
  171 + const phone = generatePhoneNumber();
  172 + const idCard = generateIdCard();
  173 +
  174 + console.log('待录入欠款的客户名称:', customerName);
  175 +
  176 + // 步骤1:先创建客户
  177 + await allure.step('创建客户', async () => {
  178 + await customerPage.gotoHome();
  179 + await customerPage.createCustomer({
  180 + name: customerName,
  181 + phone: phone,
  182 + idCard: idCard,
  183 + });
  184 + });
  185 +
  186 + // 生成欠款金额和备注
  187 + const debtAmount = generateAmount(100, 9999);
  188 + const remark = generateRemark(50);
  189 + const randomImage = getRandomImage();
  190 +
  191 + console.log('欠款金额:', debtAmount);
  192 + console.log('备注:', remark);
  193 +
  194 + // 步骤2:执行录入欠款操作
  195 + await allure.step('录入欠款', async () => {
  196 + await customerPage.recordDebt(customerName, debtAmount, {
  197 + remark: remark,
  198 + imagePath: randomImage || undefined,
  199 + });
  200 + await customerPage.attachScreenshot(testInfo, '录入欠款成功截图');
  201 + });
  202 +
  203 + // 步骤3:验证欠款录入成功
  204 + await allure.step('验证欠款录入成功', async () => {
  205 + const isRecorded = await customerPage.verifyDebtRecorded(debtAmount);
  206 + expect(isRecorded).toBeTruthy();
  207 + });
  208 + });
  209 +
  210 + test('新增客户分组', async ({ customerPage }, testInfo) => {
  211 + // 添加allure元素
  212 + await allure.epic('客户管理');
  213 + await allure.feature('客户分组');
  214 + await allure.story('新增客户分组');
  215 +
  216 + // 生成随机分组名称
  217 + const groupName = `客户分组${Date.now().toString().slice(-6)}`;
  218 + const p = customerPage.page;
  219 +
  220 + // 步骤1:进入客户管理页面并点击新增分组
  221 + await allure.step('进入客户管理页面并点击新增分组', async () => {
  222 + await customerPage.gotoHome();
  223 + await customerPage.customerMenu.click({ force: true });
  224 + await p.waitForLoadState('networkidle', { timeout: 30000 });
  225 + await p.getByText('新增分组').click();
  226 + });
  227 +
  228 + // 步骤2:填写分组信息
  229 + await allure.step('填写分组信息', async () => {
  230 + // 填写分组名称
  231 + await p.locator('uni-view').filter({ hasText: /^分组名称\*$/ }).first().click();
  232 + await p.getByRole('textbox').nth(1).fill(groupName);
  233 +
  234 + // 选择颜色/图标(点击第二个选项)
  235 + await p.locator('.uni-scroll-view-content > uni-view > uni-view:nth-child(2) > .nut-cell__title > uni-view').click();
  236 +
  237 + // 填写排序号
  238 + await p.getByRole('textbox').nth(2).click();
  239 + await p.getByRole('textbox').nth(2).fill('121');
  240 + });
  241 +
  242 + // 步骤3:保存分组
  243 + await allure.step('保存分组', async () => {
  244 + await p.getByText('保存').click();
  245 + await p.waitForTimeout(1000);
  246 + await customerPage.attachScreenshot(testInfo, '新增客户分组成功截图');
  247 + });
  248 +
  249 + // 步骤4:验证分组创建成功
  250 + await allure.step('验证分组创建成功', async () => {
  251 + const isGroupVisible = await p.getByText('大客户').isVisible();
  252 + expect(isGroupVisible).toBeTruthy();
  253 + });
  254 + });
206 255 });
207 256 \ No newline at end of file
... ...
tests/customerGroup.spec.ts 0 → 100644
  1 +import { test, expect } from '@playwright/test';
  2 +import * as allure from 'allure-js-commons';
  3 +
  4 +/**
  5 + * 客户分组测试
  6 + */
  7 +test.describe('客户分组', () => {
  8 + // 使用已保存的认证状态
  9 + test.use({ storageState: 'auth.json' });
  10 +
  11 + // 强制测试串行执行,避免并行测试之间的干扰
  12 + test.describe.configure({ mode: 'serial' });
  13 +
  14 + /**
  15 + * 生成随机分组名称(5个字以内)
  16 + */
  17 + function generateGroupName(): string {
  18 + const prefixes = ['测试', '客户', '优质', '普通', '会员', 'VIP', '大', '小', '新', '老'];
  19 + const suffixes = ['组', '户', '客', '户', '户'];
  20 + const prefix = prefixes[Math.floor(Math.random() * prefixes.length)];
  21 + const suffix = suffixes[Math.floor(Math.random() * suffixes.length)];
  22 + const timestamp = Date.now().toString().slice(-2);
  23 + const name = `${prefix}${suffix}${timestamp}`;
  24 + return name.slice(0, 5); // 确保不超过5个字
  25 + }
  26 +
  27 + /**
  28 + * 生成随机排序号
  29 + */
  30 + function generateSortNumber(): string {
  31 + return String(Math.floor(Math.random() * 999) + 1);
  32 + }
  33 +
  34 + test('新增客户分组', async ({ page }, testInfo) => {
  35 + // 添加allure元素
  36 + await allure.epic('客户管理');
  37 + await allure.feature('客户分组');
  38 + await allure.story('新增客户分组');
  39 +
  40 + // 生成随机分组名称
  41 + const groupName = generateGroupName();
  42 + console.log('新增分组名称:', groupName);
  43 +
  44 + // 步骤1:进入客户分组页面
  45 + await allure.step('进入客户分组页面', async () => {
  46 + await page.goto('/');
  47 + await page.waitForLoadState('networkidle', { timeout: 30000 });
  48 + await page.getByText('更多 >').click();
  49 + await page.waitForTimeout(500);
  50 + await page.getByText('客户分组').first().click();
  51 + await page.waitForLoadState('networkidle', { timeout: 30000 });
  52 + });
  53 +
  54 + // 步骤2:点击新建按钮
  55 + await allure.step('点击新建按钮', async () => {
  56 + await page.getByText('新建').click();
  57 + await page.waitForTimeout(500);
  58 + });
  59 +
  60 + // 步骤3:填写分组信息
  61 + await allure.step('填写分组信息', async () => {
  62 + // 填写分组名称
  63 + await page.getByRole('textbox').nth(1).click();
  64 + await page.getByRole('textbox').nth(1).fill(groupName);
  65 +
  66 + // 填写备注/排序
  67 + await page.getByRole('textbox').nth(2).click();
  68 + await page.getByRole('textbox').nth(2).fill('自动化测试分组');
  69 + });
  70 +
  71 + // 步骤4:保存分组
  72 + await allure.step('保存分组', async () => {
  73 + await page.getByText('确定').click();
  74 + await page.waitForTimeout(1000);
  75 + });
  76 +
  77 + // 步骤5:验证分组创建成功 - 使用正则匹配检查页面是否出现新增的内容
  78 + await allure.step('验证分组创建成功', async () => {
  79 + await page.waitForTimeout(1000); // 等待页面刷新
  80 + // 使用正则匹配分组名称
  81 + const isGroupVisible = await page.locator('uni-view').filter({ hasText: new RegExp(`^${groupName}$`) }).first().isVisible({ timeout: 5000 }).catch(() => false);
  82 + expect(isGroupVisible).toBeTruthy();
  83 + });
  84 + });
  85 +
  86 + test('修改客户分组', async ({ page }, testInfo) => {
  87 + // 添加allure元素
  88 + await allure.epic('客户管理');
  89 + await allure.feature('客户分组');
  90 + await allure.story('修改客户分组');
  91 +
  92 + // 先生成一个分组用于修改
  93 + const originalGroupName = generateGroupName();
  94 + const newGroupName = generateGroupName();
  95 + console.log('原分组名称:', originalGroupName);
  96 + console.log('新分组名称:', newGroupName);
  97 +
  98 + // 步骤1:进入客户分组页面并创建分组
  99 + await allure.step('进入客户分组页面并创建分组', async () => {
  100 + await page.goto('/');
  101 + await page.waitForLoadState('networkidle', { timeout: 30000 });
  102 + await page.getByText('更多 >').click();
  103 + await page.waitForTimeout(500);
  104 + await page.getByText('客户分组').first().click();
  105 + await page.waitForLoadState('networkidle', { timeout: 30000 });
  106 +
  107 + // 创建分组
  108 + await page.getByText('新建').click();
  109 + await page.waitForTimeout(500);
  110 + // 点击分组名称输入框
  111 + await page.locator('uni-view').filter({ hasText: /^分组名称\*$/ }).first().click();
  112 + await page.getByRole('textbox').nth(1).fill(originalGroupName);
  113 + await page.getByText('确定').click();
  114 + await page.waitForTimeout(1000);
  115 + });
  116 +
  117 + // 步骤2:搜索并修改分组
  118 + await allure.step('搜索并修改分组', async () => {
  119 + // 在搜索框中输入原分组名称
  120 + await page.getByRole('textbox').click();
  121 + await page.getByRole('textbox').fill(originalGroupName);
  122 +
  123 + // 点击搜索结果中的分组
  124 + await page.locator('uni-view').filter({ hasText: new RegExp(`^${originalGroupName}$`) }).first().click();
  125 + await page.waitForTimeout(500);
  126 +
  127 + // 点击编辑按钮
  128 + await page.getByText('编辑').click();
  129 + await page.waitForTimeout(500);
  130 +
  131 + // 清空分组名称并填写新名称
  132 + await page.getByRole('textbox').nth(1).click();
  133 + await page.locator('.nut-input__clear-icon').first().click();
  134 + await page.getByRole('textbox').nth(1).click();
  135 + await page.getByRole('textbox').nth(1).fill(newGroupName);
  136 +
  137 + // 直接填写新排序号(新增时未填写,无需清除)
  138 + const newSortNumber = generateSortNumber();
  139 + await page.getByRole('textbox').nth(2).click();
  140 + await page.getByRole('textbox').nth(2).fill(newSortNumber);
  141 +
  142 + await page.getByText('确定').click();
  143 + await page.waitForTimeout(1000);
  144 + });
  145 +
  146 + // 步骤3:验证分组修改成功 - 使用搜索验证修改后的内容
  147 + await allure.step('验证分组修改成功', async () => {
  148 + // 在搜索框中输入新分组名称进行验证
  149 + await page.waitForTimeout(500);
  150 + await page.getByRole('textbox').click();
  151 + await page.getByRole('textbox').fill(newGroupName);
  152 + await page.waitForTimeout(500);
  153 +
  154 + const isNewGroupVisible = await page.locator('uni-view').filter({ hasText: new RegExp(`^${newGroupName}$`) }).first().isVisible({ timeout: 5000 }).catch(() => false);
  155 + expect(isNewGroupVisible).toBeTruthy();
  156 + });
  157 + });
  158 +
  159 + test('删除客户分组', async ({ page }, testInfo) => {
  160 + // 添加allure元素
  161 + await allure.epic('客户管理');
  162 + await allure.feature('客户分组');
  163 + await allure.story('删除客户分组');
  164 +
  165 + // 先生成一个分组用于删除
  166 + const groupName = generateGroupName();
  167 + console.log('待删除分组名称:', groupName);
  168 +
  169 + // 步骤1:进入客户分组页面并创建分组
  170 + await allure.step('进入客户分组页面并创建分组', async () => {
  171 + await page.goto('/');
  172 + await page.waitForLoadState('networkidle', { timeout: 30000 });
  173 + await page.getByText('更多 >').click();
  174 + await page.waitForTimeout(500);
  175 + await page.getByText('客户分组').first().click();
  176 + await page.waitForLoadState('networkidle', { timeout: 30000 });
  177 +
  178 + // 创建分组
  179 + await page.getByText('新建').click();
  180 + await page.waitForTimeout(500);
  181 + await page.getByRole('textbox').nth(1).click();
  182 + await page.getByRole('textbox').nth(1).fill(groupName);
  183 + await page.getByText('确定').click();
  184 + await page.waitForTimeout(1000);
  185 + });
  186 +
  187 + // 步骤2:搜索并删除分组
  188 + await allure.step('搜索并删除分组', async () => {
  189 + // 在搜索框中输入分组名称
  190 + await page.getByRole('textbox').click();
  191 + await page.getByRole('textbox').fill(groupName);
  192 +
  193 + // 搜索后自动选中,直接点击删除按钮
  194 + await page.getByText('删除').click();
  195 + await page.waitForTimeout(500);
  196 +
  197 + // 确认删除
  198 + await page.getByText('确定', { exact: true }).click();
  199 + // await page.waitForTimeout(1000);
  200 +
  201 + // 点击清除按钮
  202 + await page.locator('.nut-searchbar__search-icon').click();
  203 + // 清除后等待页面刷新
  204 + await page.waitForTimeout(1000);
  205 + });
  206 +
  207 + // 步骤3:验证分组删除成功
  208 + await allure.step('验证分组删除成功', async () => {
  209 + // 搜索已删除的分组名称
  210 + await page.getByRole('textbox').fill(groupName);
  211 + await page.waitForTimeout(1000);
  212 +
  213 + // 验证搜索结果中是否还能找到该分组(找不到才是删除成功)
  214 + const count = await page.locator('uni-view').filter({ hasText: new RegExp(`^${groupName}$`) }).count();
  215 + expect(count).toBe(0);
  216 + });
  217 + });
  218 +});
... ...
tests/login.setup.ts
... ... @@ -33,6 +33,6 @@ setup(&#39;authenticate&#39;, async ({ page }) =&gt; {
33 33 // await page.waitForSelector('text=个人中心', { timeout: 60000 });
34 34  
35 35 // 5. 登录成功后,保存状态
36   -// await page.context().storageState({ path: authFile });
37   -// console.log('认证状态已保存到', authFile);
  36 + await page.context().storageState({ path: authFile });
  37 + console.log('认证状态已保存到', authFile);
38 38 });
... ...
tests/product.spec.ts
... ... @@ -37,17 +37,5 @@ test.describe(&#39;商品管理&#39;, () =&gt; {
37 37 // await productPage.expectProductVisible(productName);
38 38 });
39 39  
40   - // test('新增商品 - 使用分步操作', async ({ productPage }) => {
41   - // // 生成唯一商品名
42   - // const productName = generateUniqueProductName('苹果');
43   -
44   - // // 分步操作示例
45   - // await productPage.openAddProductForm();
46   - // await productPage.enterProductName(productName);
47   - // await productPage.selectCategory('苹果');
48   - // await productPage.clickSave();
49   -
50   - // // 验证商品创建成功
51   - // await productPage.expectProductVisible(productName);
52   - // });
  40 +
53 41 });
54 42 \ No newline at end of file
... ...
utils/dataGenerator.ts
... ... @@ -27,7 +27,11 @@ export function generateCustomerName(): string {
27 27 const familyName = familyNames[Math.floor(Math.random() * familyNames.length)];
28 28 const givenName = givenNames[Math.floor(Math.random() * givenNames.length)];
29 29  
30   - return familyName + givenName;
  30 + // 使用时间戳(后6位)+ 随机数后缀,确保不易重复,只使用数字和字母
  31 + const timestamp = Date.now().toString().slice(-6);
  32 + // const random = Math.floor(Math.random() * 100).toString().padStart(2, '0');
  33 +
  34 + return `${familyName}${givenName}${timestamp}`;
31 35 }
32 36  
33 37 // 随机生成身份证号码(18位)
... ... @@ -202,3 +206,62 @@ export function generateAmount(min: number = 1, max: number = 9999): string {
202 206 const amount = Math.floor(Math.random() * (max - min + 1)) + min;
203 207 return amount.toString();
204 208 }
  209 +
  210 +// 随机生成车牌号
  211 +export function generateLicensePlate(): string {
  212 + // 省份简称(中国各省份)
  213 + const provinces = ['京', '津', '沪', '渝', '冀', '晋', '蒙', '辽', '吉', '黑', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '桂', '琼', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新'];
  214 +
  215 + // 字母(车牌号第二位)
  216 + const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
  217 +
  218 + // 随机选择省份
  219 + const province = provinces[Math.floor(Math.random() * provinces.length)];
  220 +
  221 + // 随机选择字母
  222 + const letter = letters[Math.floor(Math.random() * letters.length)];
  223 +
  224 + // 随机生成5位数字或字母组合(第三位到第七位)
  225 + const chars = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
  226 + let suffix = '';
  227 + for (let i = 0; i < 5; i++) {
  228 + suffix += chars[Math.floor(Math.random() * chars.length)];
  229 + }
  230 +
  231 + return province + letter + suffix;
  232 +}
  233 +
  234 +// 随机生成省市区数据
  235 +export interface RegionData {
  236 + province: string;
  237 + city: string;
  238 + district: string;
  239 +}
  240 +
  241 +export function generateRegion(): RegionData {
  242 + // 省市区数据(示例数据,可根据实际业务扩展)
  243 + const regions: RegionData[] = [
  244 + { province: '山西省', city: '长治市', district: '潞城区' },
  245 + { province: '北京市', city: '北京市', district: '朝阳区' },
  246 + { province: '上海市', city: '上海市', district: '浦东新区' },
  247 + { province: '广东省', city: '广州市', district: '天河区' },
  248 + { province: '浙江省', city: '杭州市', district: '西湖区' },
  249 + { province: '江苏省', city: '南京市', district: '鼓楼区' },
  250 + { province: '四川省', city: '成都市', district: '武侯区' },
  251 + { province: '湖北省', city: '武汉市', district: '洪山区' },
  252 + { province: '湖南省', city: '长沙市', district: '岳麓区' },
  253 + { province: '河南省', city: '郑州市', district: '金水区' },
  254 + { province: '山东省', city: '济南市', district: '历下区' },
  255 + { province: '福建省', city: '福州市', district: '鼓楼区' },
  256 + { province: '安徽省', city: '合肥市', district: '蜀山区' },
  257 + { province: '江西省', city: '南昌市', district: '东湖区' },
  258 + { province: '陕西省', city: '西安市', district: '雁塔区' },
  259 + { province: '甘肃省', city: '兰州市', district: '城关区' },
  260 + { province: '云南省', city: '昆明市', district: '五华区' },
  261 + { province: '贵州省', city: '贵阳市', district: '南明区' },
  262 + { province: '河北省', city: '石家庄市', district: '长安区' },
  263 + { province: '辽宁省', city: '沈阳市', district: '和平区' },
  264 + ];
  265 +
  266 + return regions[Math.floor(Math.random() * regions.length)];
  267 +}
... ...