Commit 33bc2f2f88ab7800fbf3b853292968d1cd4b6f9c
1 parent
30d1471a
去掉硬编码
Showing
9 changed files
with
637 additions
and
63 deletions
.gitignore
| @@ -10,4 +10,5 @@ node_modules/ | @@ -10,4 +10,5 @@ node_modules/ | ||
| 10 | /.env | 10 | /.env |
| 11 | /allure-results/ | 11 | /allure-results/ |
| 12 | /plans/ | 12 | /plans/ |
| 13 | -/reports/ | ||
| 14 | \ No newline at end of file | 13 | \ No newline at end of file |
| 14 | +/reports/ | ||
| 15 | +/screenshots/ | ||
| 15 | \ No newline at end of file | 16 | \ No newline at end of file |
Jenkinsfile
0 → 100644
| 1 | +// Jenkinsfile - UI自动化测试流水线配置 | ||
| 2 | +// 使用方法:在 Jenkins 中创建 Pipeline 任务,选择 "Pipeline script from SCM" 或直接粘贴此文件内容 | ||
| 3 | + | ||
| 4 | +pipeline { | ||
| 5 | + agent any | ||
| 6 | + | ||
| 7 | + tools { | ||
| 8 | + nodejs 'NodeJS-18' // Jenkins 中配置的 Node.js 工具名称 | ||
| 9 | + allure 'Allure' // Jenkins 中配置的 Allure 工具名称 | ||
| 10 | + } | ||
| 11 | + | ||
| 12 | + environment { | ||
| 13 | + // 环境变量配置 | ||
| 14 | + NODE_ENV = 'test' | ||
| 15 | + // 测试环境 URL(可在 Jenkins 中覆盖) | ||
| 16 | + BASE_URL = credentials('test-base-url') ?: 'https://erp-pad.test.gszdtop.com' | ||
| 17 | + } | ||
| 18 | + | ||
| 19 | + parameters { | ||
| 20 | + choice( | ||
| 21 | + name: 'TEST_SUITE', | ||
| 22 | + choices: ['all', 'customer', 'product', 'sale', 'consignment'], | ||
| 23 | + description: '选择要执行的测试套件' | ||
| 24 | + ) | ||
| 25 | + choice( | ||
| 26 | + name: 'BROWSER', | ||
| 27 | + choices: ['chromium', 'firefox', 'webkit'], | ||
| 28 | + description: '选择浏览器' | ||
| 29 | + ) | ||
| 30 | + booleanParam( | ||
| 31 | + name: 'HEADLESS', | ||
| 32 | + defaultValue: true, | ||
| 33 | + description: '是否使用无头模式' | ||
| 34 | + ) | ||
| 35 | + string( | ||
| 36 | + name: 'GREP', | ||
| 37 | + defaultValue: '', | ||
| 38 | + description: '测试用例过滤(留空执行全部)' | ||
| 39 | + ) | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + stages { | ||
| 43 | + stage('Checkout') { | ||
| 44 | + steps { | ||
| 45 | + echo '📥 拉取代码...' | ||
| 46 | + git branch: 'main', url: 'https://your-git-repo-url/xfbhAutoTest.git' | ||
| 47 | + } | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + stage('Install Dependencies') { | ||
| 51 | + steps { | ||
| 52 | + echo '📦 安装依赖...' | ||
| 53 | + sh 'npm install' | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + stage('Install Playwright Browsers') { | ||
| 58 | + steps { | ||
| 59 | + echo '🌐 安装浏览器...' | ||
| 60 | + sh "npx playwright install ${params.BROWSER}" | ||
| 61 | + sh "npx playwright install-deps ${params.BROWSER}" | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + stage('Run Tests') { | ||
| 66 | + steps { | ||
| 67 | + echo '🧪 执行测试...' | ||
| 68 | + script { | ||
| 69 | + def testCommand = "npx playwright test" | ||
| 70 | + | ||
| 71 | + // 选择测试套件 | ||
| 72 | + if (params.TEST_SUITE != 'all') { | ||
| 73 | + testCommand += " tests/${params.TEST_SUITE}.spec.ts" | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + // 选择浏览器 | ||
| 77 | + testCommand += " --project=${params.BROWSER}" | ||
| 78 | + | ||
| 79 | + // 无头模式 | ||
| 80 | + if (!params.HEADLESS) { | ||
| 81 | + testCommand += " --headed" | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + // 过滤测试用例 | ||
| 85 | + if (params.GREP) { | ||
| 86 | + testCommand += " -g \"${params.GREP}\"" | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + sh testCommand | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + post { | ||
| 96 | + always { | ||
| 97 | + echo '📊 生成测试报告...' | ||
| 98 | + | ||
| 99 | + // 发布 Allure 报告 | ||
| 100 | + allure includeProperties: false, | ||
| 101 | + jdk: '', | ||
| 102 | + results: [[path: 'allure-results']] | ||
| 103 | + | ||
| 104 | + // 归档 Playwright HTML 报告 | ||
| 105 | + publishHTML(target: [ | ||
| 106 | + allowMissing: false, | ||
| 107 | + alwaysLinkToLastBuild: false, | ||
| 108 | + keepAll: true, | ||
| 109 | + reportDir: 'playwright-report', | ||
| 110 | + reportFiles: 'index.html', | ||
| 111 | + reportName: 'Playwright Report' | ||
| 112 | + ]) | ||
| 113 | + | ||
| 114 | + // 归档测试结果 | ||
| 115 | + archiveArtifacts artifacts: 'playwright-report/**, test-results/**', | ||
| 116 | + allowEmptyArchive: true | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + success { | ||
| 120 | + echo '✅ 测试执行成功!' | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + failure { | ||
| 124 | + echo '❌ 测试执行失败,请检查测试报告!' | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + unstable { | ||
| 128 | + echo '⚠️ 测试执行完成,但存在失败的用例!' | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +// ============ 定时执行配置 ============ | ||
| 134 | +// 如需定时执行,在 pipeline 块内添加以下 triggers 配置: | ||
| 135 | +// | ||
| 136 | +// triggers { | ||
| 137 | +// // 每天 6:00 执行(北京时间) | ||
| 138 | +// cron('0 22 * * *') // UTC 22:00 = 北京时间 06:00 | ||
| 139 | +// | ||
| 140 | +// // 或者使用轮询 SCM(代码变更时触发) | ||
| 141 | +// pollSCM('H/5 * * * *') // 每5分钟检查一次代码变更 | ||
| 142 | +// } | ||
| 143 | + | ||
| 144 | +// ============ 邮件通知配置 ============ | ||
| 145 | +// 如需邮件通知,在 post 块中添加: | ||
| 146 | +// | ||
| 147 | +// emailext( | ||
| 148 | +// subject: "UI自动化测试报告 - ${env.JOB_NAME} #${env.BUILD_NUMBER}", | ||
| 149 | +// body: """ | ||
| 150 | +// <h2>测试执行完成</h2> | ||
| 151 | +// <p>构建状态: ${currentBuild.currentResult}</p> | ||
| 152 | +// <p>构建链接: ${env.BUILD_URL}</p> | ||
| 153 | +// <p>Allure报告: ${env.BUILD_URL}allure/</p> | ||
| 154 | +// <p>HTML报告: ${env.BUILD_URL}Playwright_Report/</p> | ||
| 155 | +// """, | ||
| 156 | +// to: 'team@example.com', | ||
| 157 | +// from: 'jenkins@example.com' | ||
| 158 | +// ) | ||
| 0 | \ No newline at end of file | 159 | \ No newline at end of file |
pages/basePage.ts
| @@ -32,7 +32,10 @@ export abstract class BasePage { | @@ -32,7 +32,10 @@ export abstract class BasePage { | ||
| 32 | * @param path 相对路径 | 32 | * @param path 相对路径 |
| 33 | */ | 33 | */ |
| 34 | async navigate(path: string = '/'): Promise<void> { | 34 | async navigate(path: string = '/'): Promise<void> { |
| 35 | - const baseURL = process.env.BASE_URL || 'https://erp-pad.test.gszdtop.com'; | 35 | + const baseURL = process.env.BASE_URL; |
| 36 | + if (!baseURL) { | ||
| 37 | + throw new Error('BASE_URL 环境变量未设置,请设置 BASE_URL 后再运行'); | ||
| 38 | + } | ||
| 36 | await this.page.goto(`${baseURL}/#${path}`); | 39 | await this.page.goto(`${baseURL}/#${path}`); |
| 37 | } | 40 | } |
| 38 | 41 | ||
| @@ -172,7 +175,17 @@ export abstract class BasePage { | @@ -172,7 +175,17 @@ export abstract class BasePage { | ||
| 172 | * @param name 截图名称 | 175 | * @param name 截图名称 |
| 173 | */ | 176 | */ |
| 174 | async takeScreenshot(name: string): Promise<Buffer> { | 177 | async takeScreenshot(name: string): Promise<Buffer> { |
| 175 | - return this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true }); | 178 | + // 使用项目根目录下的 screenshots 文件夹 |
| 179 | + const fs = require('fs'); | ||
| 180 | + const path = require('path'); | ||
| 181 | + const screenshotsDir = path.join(process.cwd(), 'screenshots'); | ||
| 182 | + | ||
| 183 | + // 确保 screenshots 目录存在 | ||
| 184 | + if (!fs.existsSync(screenshotsDir)) { | ||
| 185 | + fs.mkdirSync(screenshotsDir, { recursive: true }); | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + return this.page.screenshot({ path: path.join(screenshotsDir, `${name}.png`), fullPage: true }); | ||
| 176 | } | 189 | } |
| 177 | 190 | ||
| 178 | /** | 191 | /** |
pages/customerPage.ts
| @@ -80,7 +80,7 @@ export class CustomerPage extends BasePage { | @@ -80,7 +80,7 @@ export class CustomerPage extends BasePage { | ||
| 80 | constructor(page: Page) { | 80 | constructor(page: Page) { |
| 81 | super(page); | 81 | super(page); |
| 82 | 82 | ||
| 83 | - this.customerMenu = page.getByText('客户管理'); | 83 | + this.customerMenu = page.getByText('客户管理').first(); |
| 84 | this.addCustomerButton = page.getByText('新增客户'); | 84 | this.addCustomerButton = page.getByText('新增客户'); |
| 85 | this.customerNameInput = page.getByRole('textbox').nth(1); | 85 | this.customerNameInput = page.getByRole('textbox').nth(1); |
| 86 | this.phoneInput = page.getByRole('spinbutton').first(); | 86 | this.phoneInput = page.getByRole('spinbutton').first(); |
| @@ -109,7 +109,7 @@ export class CustomerPage extends BasePage { | @@ -109,7 +109,7 @@ export class CustomerPage extends BasePage { | ||
| 109 | * 打开客户管理页面 | 109 | * 打开客户管理页面 |
| 110 | */ | 110 | */ |
| 111 | async openCustomerManagement(): Promise<void> { | 111 | async openCustomerManagement(): Promise<void> { |
| 112 | - await this.customerMenu.click(); | 112 | + await this.customerMenu.click({ force: true }); |
| 113 | await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | 113 | await this.page.waitForLoadState('networkidle', { timeout: 30000 }); |
| 114 | } | 114 | } |
| 115 | 115 | ||
| @@ -131,6 +131,17 @@ export class CustomerPage extends BasePage { | @@ -131,6 +131,17 @@ export class CustomerPage extends BasePage { | ||
| 131 | } | 131 | } |
| 132 | 132 | ||
| 133 | /** | 133 | /** |
| 134 | + * 清除并填写客户名称 | ||
| 135 | + * @param name 客户名称 | ||
| 136 | + */ | ||
| 137 | + async clearAndFillCustomerName(name: string): Promise<void> { | ||
| 138 | + await this.customerNameInput.click(); | ||
| 139 | + // 点击清除图标 | ||
| 140 | + await this.page.locator('.nut-input__clear-icon').first().click(); | ||
| 141 | + await this.customerNameInput.fill(name); | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + /** | ||
| 134 | * 填写手机号 | 145 | * 填写手机号 |
| 135 | * @param phone 手机号 | 146 | * @param phone 手机号 |
| 136 | */ | 147 | */ |
| @@ -140,6 +151,17 @@ export class CustomerPage extends BasePage { | @@ -140,6 +151,17 @@ export class CustomerPage extends BasePage { | ||
| 140 | } | 151 | } |
| 141 | 152 | ||
| 142 | /** | 153 | /** |
| 154 | + * 清除并填写手机号 | ||
| 155 | + * @param phone 手机号 | ||
| 156 | + */ | ||
| 157 | + async clearAndFillPhone(phone: string): Promise<void> { | ||
| 158 | + await this.phoneInput.click(); | ||
| 159 | + // 点击手机号清除图标 | ||
| 160 | + await this.page.locator('uni-view:nth-child(3) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__clear > .nut-input__clear-icon').click(); | ||
| 161 | + await this.phoneInput.fill(phone); | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + /** | ||
| 143 | * 填写身份证号 | 165 | * 填写身份证号 |
| 144 | * @param idCard 身份证号 | 166 | * @param idCard 身份证号 |
| 145 | */ | 167 | */ |
| @@ -149,6 +171,17 @@ export class CustomerPage extends BasePage { | @@ -149,6 +171,17 @@ export class CustomerPage extends BasePage { | ||
| 149 | } | 171 | } |
| 150 | 172 | ||
| 151 | /** | 173 | /** |
| 174 | + * 清除并填写身份证号 | ||
| 175 | + * @param idCard 身份证号 | ||
| 176 | + */ | ||
| 177 | + async clearAndFillIdCard(idCard: string): Promise<void> { | ||
| 178 | + await this.idCardInput.click(); | ||
| 179 | + // 点击身份证清除图标 | ||
| 180 | + await this.page.locator('uni-view:nth-child(4) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__clear > .nut-input__clear-icon').click(); | ||
| 181 | + await this.idCardInput.fill(idCard); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + /** | ||
| 152 | * 选择客户分组(选择"普通客户") | 185 | * 选择客户分组(选择"普通客户") |
| 153 | */ | 186 | */ |
| 154 | async selectCustomerGroup(): Promise<void> { | 187 | async selectCustomerGroup(): Promise<void> { |
| @@ -320,4 +353,246 @@ export class CustomerPage extends BasePage { | @@ -320,4 +353,246 @@ export class CustomerPage extends BasePage { | ||
| 320 | return false; | 353 | return false; |
| 321 | } | 354 | } |
| 322 | } | 355 | } |
| 323 | -} | ||
| 324 | \ No newline at end of file | 356 | \ No newline at end of file |
| 357 | + | ||
| 358 | + // ==================== 修改/删除客户相关方法 ==================== | ||
| 359 | + | ||
| 360 | + /** | ||
| 361 | + * 搜索客户 | ||
| 362 | + * @param customerName 客户名称 | ||
| 363 | + */ | ||
| 364 | + async searchCustomer(customerName: string): Promise<void> { | ||
| 365 | + // 点击搜索框区域(使用 force: true 绕过遮罩层) | ||
| 366 | + await this.page.locator('uni-view').filter({ hasText: /^客户名称\/手机号$/ }).nth(2).click({ force: true }); | ||
| 367 | + // 在输入框中填写客户名称 | ||
| 368 | + await this.page.getByRole('textbox').fill(customerName); | ||
| 369 | + await this.page.waitForTimeout(1000); | ||
| 370 | + } | ||
| 371 | + | ||
| 372 | + /** | ||
| 373 | + * 点击客户进入详情页面 | ||
| 374 | + * @param customerName 客户名称 | ||
| 375 | + */ | ||
| 376 | + async clickCustomerItem(customerName: string): Promise<void> { | ||
| 377 | + await this.page.getByText(customerName).first().click(); | ||
| 378 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 379 | + } | ||
| 380 | + | ||
| 381 | + /** | ||
| 382 | + * 点击编辑按钮 | ||
| 383 | + */ | ||
| 384 | + async clickEditButton(): Promise<void> { | ||
| 385 | + await this.page.getByText('修改客户').click(); | ||
| 386 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 387 | + } | ||
| 388 | + | ||
| 389 | + /** | ||
| 390 | + * 点击删除按钮 | ||
| 391 | + */ | ||
| 392 | + async clickDeleteButton(): Promise<void> { | ||
| 393 | + await this.page.getByText('删除客户').click(); | ||
| 394 | + await this.page.waitForTimeout(500); | ||
| 395 | + } | ||
| 396 | + | ||
| 397 | + /** | ||
| 398 | + * 确认删除 | ||
| 399 | + */ | ||
| 400 | + async confirmDelete(): Promise<void> { | ||
| 401 | + await this.page.getByText('确定', { exact: true }).click(); | ||
| 402 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 403 | + } | ||
| 404 | + | ||
| 405 | + /** | ||
| 406 | + * 修改客户 - 复用新增方法 | ||
| 407 | + * @param originalName 原客户名称(用于搜索) | ||
| 408 | + * @param customerInfo 新的客户信息 | ||
| 409 | + * @param options 可选配置 | ||
| 410 | + */ | ||
| 411 | + async updateCustomer( | ||
| 412 | + originalName: string, | ||
| 413 | + customerInfo: { | ||
| 414 | + name: string; | ||
| 415 | + phone: string; | ||
| 416 | + idCard: string; | ||
| 417 | + detailedAddress?: string; | ||
| 418 | + }, | ||
| 419 | + options?: { | ||
| 420 | + creditLimit?: string; | ||
| 421 | + licensePlate?: string; | ||
| 422 | + province?: string; | ||
| 423 | + city?: string; | ||
| 424 | + district?: string; | ||
| 425 | + imagePath?: string; | ||
| 426 | + } | ||
| 427 | + ): Promise<void> { | ||
| 428 | + // 先返回首页确保页面状态干净(避免遮罩层阻挡) | ||
| 429 | + await this.gotoHome(); | ||
| 430 | + // 打开客户管理并搜索 | ||
| 431 | + await this.openCustomerManagement(); | ||
| 432 | + await this.searchCustomer(originalName); | ||
| 433 | + await this.clickCustomerItem(originalName); | ||
| 434 | + await this.clickEditButton(); | ||
| 435 | + | ||
| 436 | + // 使用清除并填写方法修改信息 | ||
| 437 | + await this.clearAndFillCustomerName(customerInfo.name); | ||
| 438 | + await this.clearAndFillPhone(customerInfo.phone); | ||
| 439 | + await this.clearAndFillIdCard(customerInfo.idCard); | ||
| 440 | + | ||
| 441 | + if (options?.creditLimit) { | ||
| 442 | + // 清除并填写赊欠额度(使用 force: true 绕过遮罩层) | ||
| 443 | + await this.creditLimitInput.click({ force: true }); | ||
| 444 | + 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 }); | ||
| 445 | + await this.creditLimitInput.fill(options.creditLimit); | ||
| 446 | + } | ||
| 447 | + | ||
| 448 | + if (options?.licensePlate) { | ||
| 449 | + await this.fillLicensePlate(options.licensePlate); | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + if (options?.province && options?.city && options?.district) { | ||
| 453 | + await this.selectRegion(options.province, options.city, options.district); | ||
| 454 | + } | ||
| 455 | + | ||
| 456 | + if (customerInfo.detailedAddress) { | ||
| 457 | + await this.fillDetailedAddress(customerInfo.detailedAddress); | ||
| 458 | + } | ||
| 459 | + | ||
| 460 | + if (options?.imagePath) { | ||
| 461 | + await this.uploadCustomerImage(options.imagePath); | ||
| 462 | + } | ||
| 463 | + | ||
| 464 | + await this.saveButton.click(); | ||
| 465 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 466 | + } | ||
| 467 | + | ||
| 468 | + /** | ||
| 469 | + * 删除客户 | ||
| 470 | + * @param customerName 客户名称 | ||
| 471 | + */ | ||
| 472 | + async deleteCustomer(customerName: string): Promise<void> { | ||
| 473 | + // 先返回首页确保页面状态干净(避免遮罩层阻挡) | ||
| 474 | + await this.gotoHome(); | ||
| 475 | + await this.openCustomerManagement(); | ||
| 476 | + await this.searchCustomer(customerName); | ||
| 477 | + await this.clickCustomerItem(customerName); | ||
| 478 | + await this.clickDeleteButton(); | ||
| 479 | + await this.confirmDelete(); | ||
| 480 | + } | ||
| 481 | + | ||
| 482 | + /** | ||
| 483 | + * 验证客户是否已被删除 | ||
| 484 | + * @param customerName 客户名称 | ||
| 485 | + */ | ||
| 486 | + async verifyCustomerDeleted(customerName: string): Promise<boolean> { | ||
| 487 | + await this.gotoHome(); | ||
| 488 | + await this.openCustomerManagement(); | ||
| 489 | + await this.searchCustomer(customerName); | ||
| 490 | + | ||
| 491 | + const count = await this.page.getByText(customerName).count(); | ||
| 492 | + return count === 0; | ||
| 493 | + } | ||
| 494 | + | ||
| 495 | + // ==================== 录入欠款相关方法 ==================== | ||
| 496 | + | ||
| 497 | + /** | ||
| 498 | + * 点击录入欠款按钮 | ||
| 499 | + */ | ||
| 500 | + async clickRecordDebt(): Promise<void> { | ||
| 501 | + await this.page.getByText('录入欠款').click(); | ||
| 502 | + await this.page.waitForTimeout(500); | ||
| 503 | + } | ||
| 504 | + | ||
| 505 | + /** | ||
| 506 | + * 选择欠款类型 | ||
| 507 | + * @param typeIndex 类型索引(默认选择第三个,索引为2) | ||
| 508 | + */ | ||
| 509 | + async selectDebtType(typeIndex: number = 2): Promise<void> { | ||
| 510 | + 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(); | ||
| 511 | + await this.page.locator('uni-view').filter({ hasText: /^2$/ }).nth(typeIndex).click(); | ||
| 512 | + } | ||
| 513 | + | ||
| 514 | + /** | ||
| 515 | + * 填写欠款金额 | ||
| 516 | + * @param amount 欠款金额 | ||
| 517 | + */ | ||
| 518 | + async fillDebtAmount(amount: string): Promise<void> { | ||
| 519 | + await this.page.getByRole('spinbutton').click(); | ||
| 520 | + await this.page.getByRole('spinbutton').fill(amount); | ||
| 521 | + } | ||
| 522 | + | ||
| 523 | + /** | ||
| 524 | + * 填写欠款备注 | ||
| 525 | + * @param remark 备注 | ||
| 526 | + */ | ||
| 527 | + async fillDebtRemark(remark: string): Promise<void> { | ||
| 528 | + await this.page.getByRole('textbox').nth(4).click(); | ||
| 529 | + await this.page.getByRole('textbox').nth(4).fill(remark); | ||
| 530 | + } | ||
| 531 | + | ||
| 532 | + /** | ||
| 533 | + * 保存欠款 | ||
| 534 | + */ | ||
| 535 | + async saveDebt(): Promise<void> { | ||
| 536 | + await this.page.getByText('保存', { exact: true }).click(); | ||
| 537 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 538 | + } | ||
| 539 | + | ||
| 540 | + /** | ||
| 541 | + * 录入欠款 - 完整流程 | ||
| 542 | + * @param customerName 客户名称 | ||
| 543 | + * @param amount 欠款金额 | ||
| 544 | + * @param options 可选配置 | ||
| 545 | + */ | ||
| 546 | + async recordDebt( | ||
| 547 | + customerName: string, | ||
| 548 | + amount: string, | ||
| 549 | + options?: { | ||
| 550 | + typeIndex?: number; // 欠款类型索引 | ||
| 551 | + remark?: string; // 备注 | ||
| 552 | + imagePath?: string; // 图片路径 | ||
| 553 | + } | ||
| 554 | + ): Promise<void> { | ||
| 555 | + // 先返回首页确保页面状态干净(避免遮罩层阻挡) | ||
| 556 | + await this.gotoHome(); | ||
| 557 | + // 打开客户管理并搜索 | ||
| 558 | + await this.openCustomerManagement(); | ||
| 559 | + await this.searchCustomer(customerName); | ||
| 560 | + await this.clickCustomerItem(customerName); | ||
| 561 | + | ||
| 562 | + // 点击录入欠款 | ||
| 563 | + await this.clickRecordDebt(); | ||
| 564 | + | ||
| 565 | + // 选择欠款类型 | ||
| 566 | + if (options?.typeIndex !== undefined) { | ||
| 567 | + await this.selectDebtType(options.typeIndex); | ||
| 568 | + } | ||
| 569 | + | ||
| 570 | + // 填写欠款金额 | ||
| 571 | + await this.fillDebtAmount(amount); | ||
| 572 | + | ||
| 573 | + // 填写备注 | ||
| 574 | + if (options?.remark) { | ||
| 575 | + await this.fillDebtRemark(options.remark); | ||
| 576 | + } | ||
| 577 | + | ||
| 578 | + // 上传图片 - 使用基类封装的方法 | ||
| 579 | + if (options?.imagePath) { | ||
| 580 | + await this.uploadCustomerImage(options.imagePath); | ||
| 581 | + } | ||
| 582 | + | ||
| 583 | + // 保存 | ||
| 584 | + await this.saveDebt(); | ||
| 585 | + } | ||
| 586 | + | ||
| 587 | + /** | ||
| 588 | + * 验证欠款是否录入成功 | ||
| 589 | + * @param amount 欠款金额 | ||
| 590 | + */ | ||
| 591 | + async verifyDebtRecorded(amount: string): Promise<boolean> { | ||
| 592 | + try { | ||
| 593 | + await this.page.getByText(`欠款:${amount}.00元`).waitFor({ timeout: 10000 }); | ||
| 594 | + return true; | ||
| 595 | + } catch { | ||
| 596 | + return false; | ||
| 597 | + } | ||
| 598 | + } | ||
| 599 | +} |
pages/loginPage.ts
| @@ -131,16 +131,20 @@ export class LoginPage extends BasePage { | @@ -131,16 +131,20 @@ export class LoginPage extends BasePage { | ||
| 131 | /** | 131 | /** |
| 132 | * 完整的登录流程(半自动) | 132 | * 完整的登录流程(半自动) |
| 133 | * @param phone 手机号 | 133 | * @param phone 手机号 |
| 134 | - * @param userName 期望的用户名(用于验证登录成功) | 134 | + * @param userName 期望的用户名(用于验证登录成功,可选,默认从环境变量 TEST_USER_NAME 获取) |
| 135 | * @param authFilePath 认证文件保存路径 | 135 | * @param authFilePath 认证文件保存路径 |
| 136 | */ | 136 | */ |
| 137 | async performLogin( | 137 | async performLogin( |
| 138 | - phone: string, | ||
| 139 | - userName: string = '赵xt', | 138 | + phone: string, |
| 139 | + userName?: string, | ||
| 140 | authFilePath: string = 'auth.json' | 140 | authFilePath: string = 'auth.json' |
| 141 | ): Promise<void> { | 141 | ): Promise<void> { |
| 142 | + const finalUserName = userName ?? process.env.TEST_USER_NAME; | ||
| 143 | + if (!finalUserName) { | ||
| 144 | + throw new Error('userName 参数未提供且 TEST_USER_NAME 环境变量未设置,请设置其中之一后再运行'); | ||
| 145 | + } | ||
| 142 | await this.loginWithPhone(phone); | 146 | await this.loginWithPhone(phone); |
| 143 | - await this.waitForLoginSuccess(userName); | 147 | + await this.waitForLoginSuccess(finalUserName); |
| 144 | await this.saveAuthState(authFilePath); | 148 | await this.saveAuthState(authFilePath); |
| 145 | } | 149 | } |
| 146 | } | 150 | } |
| 147 | \ No newline at end of file | 151 | \ No newline at end of file |
scripts/save-auth.ts
| 1 | // 半自动登录,登录存放auth.json | 1 | // 半自动登录,登录存放auth.json |
| 2 | import { test, expect } from '@playwright/test'; | 2 | import { test, expect } from '@playwright/test'; |
| 3 | 3 | ||
| 4 | +// 从环境变量获取配置 | ||
| 5 | +const TEST_PHONE = process.env.phone; | ||
| 6 | +const TEST_USER_NAME = process.env.TEST_USER_NAME; | ||
| 7 | + | ||
| 8 | +if (!TEST_PHONE) { | ||
| 9 | + throw new Error('phone 环境变量未设置,请设置 phone 后再运行'); | ||
| 10 | +} | ||
| 11 | +if (!TEST_USER_NAME) { | ||
| 12 | + throw new Error('TEST_USER_NAME 环境变量未设置,请设置 TEST_USER_NAME 后再运行'); | ||
| 13 | +} | ||
| 14 | + | ||
| 4 | test('半自动登录(自动填手机号,手动输验证码)', async ({ page }) => { | 15 | test('半自动登录(自动填手机号,手动输验证码)', async ({ page }) => { |
| 5 | await page.goto('/#/pages/login/index'); | 16 | await page.goto('/#/pages/login/index'); |
| 6 | await page.getByText('手机号登录/注册').click(); | 17 | await page.getByText('手机号登录/注册').click(); |
| 7 | await page.getByText('确定').click(); | 18 | await page.getByText('确定').click(); |
| 8 | // --- 这里执行登录操作 --- | 19 | // --- 这里执行登录操作 --- |
| 9 | await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').click(); | 20 | await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').click(); |
| 10 | - await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').fill('13548301969'); | 21 | + await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').fill(TEST_PHONE); |
| 11 | // 此处假设出现了验证码,你需要手动输入或处理 | 22 | // 此处假设出现了验证码,你需要手动输入或处理 |
| 12 | await page.locator('uni-button').filter({ hasText: '获取验证码' }).click(); | 23 | await page.locator('uni-button').filter({ hasText: '获取验证码' }).click(); |
| 13 | console.log('请手动输入验证码,然后点击登录按钮...'); | 24 | console.log('请手动输入验证码,然后点击登录按钮...'); |
| 14 | await page.locator('uni-button').filter({ hasText: '登录' }).click(); | 25 | await page.locator('uni-button').filter({ hasText: '登录' }).click(); |
| 15 | // --- 等待登录成功,跳转到首页 --- | 26 | // --- 等待登录成功,跳转到首页 --- |
| 16 | - await expect(page.getByText('赵xt')).toBeVisible({ timeout: 0 }); | 27 | + await expect(page.getByText(TEST_USER_NAME)).toBeVisible({ timeout: 0 }); |
| 17 | 28 | ||
| 18 | // 4. 保存存储状态(Cookie 和 LocalStorage) | 29 | // 4. 保存存储状态(Cookie 和 LocalStorage) |
| 19 | await page.context().storageState({ path: 'auth.json' }); | 30 | await page.context().storageState({ path: 'auth.json' }); |
tests/customer.spec.ts
| 1 | import { test, expect } from '../fixtures'; | 1 | import { test, expect } from '../fixtures'; |
| 2 | -import { generateCustomerName, generateIdCard, generatePhoneNumber, generateDetailedAddress, generateCustomerInfo, getRandomImage } from '../utils/dataGenerator'; | 2 | +import { generateCustomerName, generateIdCard, generatePhoneNumber, generateDetailedAddress, generateCustomerInfo, getRandomImage, generateRemark, generateAmount } from '../utils/dataGenerator'; |
| 3 | import * as allure from 'allure-js-commons'; | 3 | import * as allure from 'allure-js-commons'; |
| 4 | 4 | ||
| 5 | /** | 5 | /** |
| 6 | * 客户管理测试 | 6 | * 客户管理测试 |
| 7 | */ | 7 | */ |
| 8 | +// 新增客户 | ||
| 8 | test.describe('客户管理', () => { | 9 | test.describe('客户管理', () => { |
| 9 | // 使用已保存的认证状态 | 10 | // 使用已保存的认证状态 |
| 10 | test.use({ storageState: 'auth.json' }); | 11 | test.use({ storageState: 'auth.json' }); |
| @@ -61,75 +62,145 @@ test.describe('客户管理', () => { | @@ -61,75 +62,145 @@ test.describe('客户管理', () => { | ||
| 61 | }); | 62 | }); |
| 62 | }); | 63 | }); |
| 63 | 64 | ||
| 64 | -// test('新增客户 - 仅必填信息', async ({ customerPage }, testInfo) => { | 65 | +// test('修改客户', async ({ customerPage }, testInfo) => { |
| 65 | // // 添加allure元素 | 66 | // // 添加allure元素 |
| 66 | // await allure.epic('客户管理'); | 67 | // await allure.epic('客户管理'); |
| 67 | // await allure.feature('客户信息'); | 68 | // await allure.feature('客户信息'); |
| 68 | -// await allure.story('创建新客户(仅必填信息)'); | 69 | +// await allure.story('修改客户'); |
| 69 | 70 | ||
| 70 | -// // 步骤1:生成随机客户信息 | ||
| 71 | -// const customerInfo = await allure.step('生成随机客户信息', async (step) => { | ||
| 72 | -// const info = generateCustomerInfo(); | ||
| 73 | -// console.log('生成的客户信息:', info); | ||
| 74 | -// return info; | ||
| 75 | -// }); | 71 | +// // 先创建一个客户用于修改 |
| 72 | +// const originalName = generateCustomerName(); | ||
| 73 | +// const originalPhone = generatePhoneNumber(); | ||
| 74 | +// const originalIdCard = generateIdCard(); | ||
| 75 | + | ||
| 76 | +// console.log('原客户名称:', originalName); | ||
| 76 | 77 | ||
| 77 | -// // 步骤2:执行新增客户操作 | ||
| 78 | -// await allure.step('填写并提交客户表单(仅必填信息)', async () => { | 78 | +// // 步骤1:先创建客户 |
| 79 | +// await allure.step('创建待修改的客户', async () => { | ||
| 79 | // await customerPage.gotoHome(); | 80 | // await customerPage.gotoHome(); |
| 80 | // await customerPage.createCustomer({ | 81 | // await customerPage.createCustomer({ |
| 81 | -// name: customerInfo.name, | ||
| 82 | -// phone: customerInfo.phone, | ||
| 83 | -// idCard: customerInfo.idCard, | 82 | +// name: originalName, |
| 83 | +// phone: originalPhone, | ||
| 84 | +// idCard: originalIdCard, | ||
| 84 | // }); | 85 | // }); |
| 85 | -// await customerPage.attachScreenshot(testInfo, '新增客户成功截图'); | ||
| 86 | // }); | 86 | // }); |
| 87 | 87 | ||
| 88 | -// // 步骤3:验证客户创建成功 | ||
| 89 | -// await allure.step('验证客户创建成功', async () => { | ||
| 90 | -// const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name); | ||
| 91 | -// expect(isCreated).toBeTruthy(); | 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(); | ||
| 92 | // }); | 121 | // }); |
| 93 | // }); | 122 | // }); |
| 94 | 123 | ||
| 95 | -// test('新增客户 - 带详细地址', async ({ customerPage }, testInfo) => { | 124 | +// test('删除客户', async ({ customerPage }, testInfo) => { |
| 96 | // // 添加allure元素 | 125 | // // 添加allure元素 |
| 97 | // await allure.epic('客户管理'); | 126 | // await allure.epic('客户管理'); |
| 98 | // await allure.feature('客户信息'); | 127 | // await allure.feature('客户信息'); |
| 99 | -// await allure.story('创建新客户(带详细地址)'); | ||
| 100 | - | ||
| 101 | -// // 步骤1:生成随机客户信息 | ||
| 102 | -// const customerInfo = await allure.step('生成随机客户信息', async (step) => { | ||
| 103 | -// const name = generateCustomerName(); | ||
| 104 | -// const phone = generatePhoneNumber(); | ||
| 105 | -// const idCard = generateIdCard(); | ||
| 106 | -// const detailedAddress = generateDetailedAddress(); | ||
| 107 | - | ||
| 108 | -// console.log('生成的客户信息:', { name, phone, idCard, detailedAddress }); | ||
| 109 | - | ||
| 110 | -// return { name, phone, idCard, detailedAddress }; | 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, '删除客户成功截图'); | ||
| 111 | // }); | 151 | // }); |
| 112 | 152 | ||
| 113 | -// // 步骤2:执行新增客户操作 | ||
| 114 | -// await allure.step('填写并提交客户表单', async () => { | 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 () => { | ||
| 115 | // await customerPage.gotoHome(); | 175 | // await customerPage.gotoHome(); |
| 116 | -// await customerPage.createCustomer( | ||
| 117 | -// { | ||
| 118 | -// name: customerInfo.name, | ||
| 119 | -// phone: customerInfo.phone, | ||
| 120 | -// idCard: customerInfo.idCard, | ||
| 121 | -// }, | ||
| 122 | -// { | ||
| 123 | -// creditLimit: '10000', | ||
| 124 | -// } | ||
| 125 | -// ); | ||
| 126 | -// await customerPage.attachScreenshot(testInfo, '新增客户成功截图'); | 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, '录入欠款成功截图'); | ||
| 127 | // }); | 198 | // }); |
| 128 | 199 | ||
| 129 | -// // 步骤3:验证客户创建成功 | ||
| 130 | -// await allure.step('验证客户创建成功', async () => { | ||
| 131 | -// const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name); | ||
| 132 | -// expect(isCreated).toBeTruthy(); | 200 | +// // 步骤3:验证欠款录入成功 |
| 201 | +// await allure.step('验证欠款录入成功', async () => { | ||
| 202 | +// const isRecorded = await customerPage.verifyDebtRecorded(debtAmount); | ||
| 203 | +// expect(isRecorded).toBeTruthy(); | ||
| 133 | // }); | 204 | // }); |
| 134 | // }); | 205 | // }); |
| 135 | }); | 206 | }); |
| 136 | \ No newline at end of file | 207 | \ No newline at end of file |
tests/login.setup.ts
| @@ -3,6 +3,18 @@ import path from 'path'; | @@ -3,6 +3,18 @@ import path from 'path'; | ||
| 3 | 3 | ||
| 4 | const authFile = path.join(__dirname, '../auth.json'); | 4 | const authFile = path.join(__dirname, '../auth.json'); |
| 5 | 5 | ||
| 6 | +// 从环境变量获取配置 | ||
| 7 | +const BASE_URL = process.env.BASE_URL; | ||
| 8 | +const TEST_USER_NAME = process.env.TEST_USER_NAME; | ||
| 9 | + | ||
| 10 | +if (!BASE_URL) { | ||
| 11 | + throw new Error('BASE_URL 环境变量未设置,请设置 BASE_URL 后再运行'); | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +if (!TEST_USER_NAME) { | ||
| 15 | + throw new Error('TEST_USER_NAME 环境变量未设置,请设置 TEST_USER_NAME 后再运行'); | ||
| 16 | +} | ||
| 17 | + | ||
| 6 | setup('authenticate', async ({ page }) => { | 18 | setup('authenticate', async ({ page }) => { |
| 7 | // 1. 先检查 auth.json 是否存在 | 19 | // 1. 先检查 auth.json 是否存在 |
| 8 | // 如果存在,并且你想让它自动跳过,可以加一个判断。这里简单起见,总是执行手动登录。 | 20 | // 如果存在,并且你想让它自动跳过,可以加一个判断。这里简单起见,总是执行手动登录。 |
| @@ -11,13 +23,13 @@ setup('authenticate', async ({ page }) => { | @@ -11,13 +23,13 @@ setup('authenticate', async ({ page }) => { | ||
| 11 | console.log('开始执行认证设置...'); | 23 | console.log('开始执行认证设置...'); |
| 12 | 24 | ||
| 13 | // 2. 访问登录页 | 25 | // 2. 访问登录页 |
| 14 | - await page.goto('https://erp-pad.test.gszdtop.com/#/'); | 26 | + await page.goto(`${BASE_URL}/#/`); |
| 15 | 27 | ||
| 16 | // 3. 手动登录(这里可以加一些提示) | 28 | // 3. 手动登录(这里可以加一些提示) |
| 17 | //console.log('请手动完成手机验证码登录...'); | 29 | //console.log('请手动完成手机验证码登录...'); |
| 18 | 30 | ||
| 19 | // 4. 等待登录成功,检测某个登录后才会出现的元素 | 31 | // 4. 等待登录成功,检测某个登录后才会出现的元素 |
| 20 | - await expect(page.getByText('赵xt')).toBeVisible(); | 32 | + await expect(page.getByText(TEST_USER_NAME)).toBeVisible(); |
| 21 | // await page.waitForSelector('text=个人中心', { timeout: 60000 }); | 33 | // await page.waitForSelector('text=个人中心', { timeout: 60000 }); |
| 22 | 34 | ||
| 23 | // 5. 登录成功后,保存状态 | 35 | // 5. 登录成功后,保存状态 |
utils/dataGenerator.ts
| @@ -173,3 +173,32 @@ export function getRandomImage(imageDir: string = 'test-data/img'): string | nul | @@ -173,3 +173,32 @@ export function getRandomImage(imageDir: string = 'test-data/img'): string | nul | ||
| 173 | return null; | 173 | return null; |
| 174 | } | 174 | } |
| 175 | } | 175 | } |
| 176 | + | ||
| 177 | +// 随机生成备注(50字以内) | ||
| 178 | +export function generateRemark(maxLength: number = 50): string { | ||
| 179 | + const phrases = [ | ||
| 180 | + '自动化测试数据', | ||
| 181 | + '测试备注信息', | ||
| 182 | + '系统自动生成', | ||
| 183 | + '功能验证数据', | ||
| 184 | + '接口测试记录', | ||
| 185 | + '数据录入测试', | ||
| 186 | + '业务流程验证', | ||
| 187 | + '质量保证测试', | ||
| 188 | + '回归测试数据', | ||
| 189 | + '冒烟测试记录' | ||
| 190 | + ]; | ||
| 191 | + | ||
| 192 | + const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)]; | ||
| 193 | + const timestamp = Date.now().toString().slice(-6); | ||
| 194 | + const remark = `${randomPhrase}_${timestamp}`; | ||
| 195 | + | ||
| 196 | + // 确保不超过最大长度 | ||
| 197 | + return remark.slice(0, maxLength); | ||
| 198 | +} | ||
| 199 | + | ||
| 200 | +// 随机生成金额(1-9999) | ||
| 201 | +export function generateAmount(min: number = 1, max: number = 9999): string { | ||
| 202 | + const amount = Math.floor(Math.random() * (max - min + 1)) + min; | ||
| 203 | + return amount.toString(); | ||
| 204 | +} |