order-management.page.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. import { Page, Locator } from '@playwright/test';
  2. import { selectRadixOption } from '@d8d/e2e-test-utils';
  3. /**
  4. * 订单状态常量
  5. */
  6. export const ORDER_STATUS = {
  7. DRAFT: 'draft',
  8. CONFIRMED: 'confirmed',
  9. IN_PROGRESS: 'in_progress',
  10. COMPLETED: 'completed',
  11. } as const;
  12. /**
  13. * 订单状态类型
  14. */
  15. export type OrderStatus = typeof ORDER_STATUS[keyof typeof ORDER_STATUS];
  16. /**
  17. * 订单状态显示名称映射
  18. */
  19. export const ORDER_STATUS_LABELS: Record<OrderStatus, string> = {
  20. draft: '草稿',
  21. confirmed: '已确认',
  22. in_progress: '进行中',
  23. completed: '已完成',
  24. } as const;
  25. /**
  26. * 工作状态常量
  27. */
  28. export const WORK_STATUS = {
  29. NOT_EMPLOYED: 'not_employed',
  30. PENDING: 'pending',
  31. EMPLOYED: 'employed',
  32. RESIGNED: 'resigned',
  33. } as const;
  34. /**
  35. * 工作状态类型
  36. */
  37. export type WorkStatus = typeof WORK_STATUS[keyof typeof WORK_STATUS];
  38. /**
  39. * 工作状态显示名称映射
  40. */
  41. export const WORK_STATUS_LABELS: Record<WorkStatus, string> = {
  42. not_employed: '未就业',
  43. pending: '待就业',
  44. employed: '已就业',
  45. resigned: '已离职',
  46. } as const;
  47. /**
  48. * 订单数据接口
  49. */
  50. export interface OrderData {
  51. /** 订单名称 */
  52. name: string;
  53. /** 预计开始日期 */
  54. expectedStartDate?: string;
  55. /** 平台ID */
  56. platformId?: number;
  57. /** 平台名称 */
  58. platformName?: string;
  59. /** 公司ID */
  60. companyId?: number;
  61. /** 公司名称 */
  62. companyName?: string;
  63. /** 渠道ID */
  64. channelId?: number;
  65. /** 渠道名称 */
  66. channelName?: string;
  67. /** 订单状态 */
  68. status?: OrderStatus;
  69. /** 工作状态 */
  70. workStatus?: WorkStatus;
  71. }
  72. /**
  73. * 订单人员数据接口
  74. */
  75. export interface OrderPersonData {
  76. /** 残疾人ID */
  77. disabledPersonId: number;
  78. /** 残疾人姓名 */
  79. disabledPersonName?: string;
  80. /** 入职日期 */
  81. hireDate?: string;
  82. /** 薪资 */
  83. salary?: number;
  84. /** 工作状态 */
  85. workStatus?: WorkStatus;
  86. /** 实际入职日期 */
  87. actualHireDate?: string;
  88. /** 离职日期 */
  89. resignDate?: string;
  90. }
  91. /**
  92. * 网络响应数据接口
  93. */
  94. export interface NetworkResponse {
  95. /** 请求URL */
  96. url: string;
  97. /** 请求方法 */
  98. method: string;
  99. /** 响应状态码 */
  100. status: number;
  101. /** 是否成功 */
  102. ok: boolean;
  103. /** 响应头 */
  104. responseHeaders: Record<string, string>;
  105. /** 响应体 */
  106. responseBody: unknown;
  107. }
  108. /**
  109. * 表单提交结果接口
  110. */
  111. export interface FormSubmitResult {
  112. /** 提交是否成功 */
  113. success: boolean;
  114. /** 是否有错误 */
  115. hasError: boolean;
  116. /** 是否有成功消息 */
  117. hasSuccess: boolean;
  118. /** 错误消息 */
  119. errorMessage?: string;
  120. /** 成功消息 */
  121. successMessage?: string;
  122. /** 网络响应列表 */
  123. responses?: NetworkResponse[];
  124. }
  125. /**
  126. * 订单管理 Page Object
  127. *
  128. * 用于订单管理功能的 E2E 测试
  129. * 页面路径: /admin/orders(待确认)
  130. *
  131. * @example
  132. * ```typescript
  133. * const orderPage = new OrderManagementPage(page);
  134. * await orderPage.goto();
  135. * await orderPage.createOrder({ name: '测试订单' });
  136. * ```
  137. */
  138. export class OrderManagementPage {
  139. readonly page: Page;
  140. // ===== 页面级选择器 =====
  141. /** 页面标题 */
  142. readonly pageTitle: Locator;
  143. /** 新增订单按钮 */
  144. readonly addOrderButton: Locator;
  145. /** 订单列表表格 */
  146. readonly orderTable: Locator;
  147. /** 搜索输入框 */
  148. readonly searchInput: Locator;
  149. /** 搜索按钮 */
  150. readonly searchButton: Locator;
  151. constructor(page: Page) {
  152. this.page = page;
  153. // 初始化页面级选择器
  154. // 使用更精确的选择器来定位页面标题(避免与侧边栏按钮冲突)
  155. this.pageTitle = page.locator('[data-slot="card-title"]').getByText('订单管理', { exact: true });
  156. // 使用 data-testid 定位创建订单按钮(按钮文本是"创建订单"不是"新增订单")
  157. this.addOrderButton = page.getByTestId('create-order-button');
  158. this.orderTable = page.locator('table');
  159. // 使用 data-testid 定位搜索输入框
  160. this.searchInput = page.getByTestId('search-order-name-input');
  161. // 使用 data-testid 定位搜索按钮
  162. this.searchButton = page.getByTestId('search-button');
  163. }
  164. // ===== 导航和基础验证 =====
  165. /**
  166. * 导航到订单管理页面
  167. */
  168. async goto() {
  169. await this.page.goto('/admin/orders');
  170. await this.page.waitForLoadState('domcontentloaded');
  171. // 等待页面标题出现
  172. await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
  173. // 等待表格数据加载
  174. await this.page.waitForSelector('table tbody tr', { state: 'visible', timeout: 20000 });
  175. await this.expectToBeVisible();
  176. }
  177. /**
  178. * 验证页面关键元素可见
  179. */
  180. async expectToBeVisible() {
  181. await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
  182. await this.addOrderButton.waitFor({ state: 'visible', timeout: 10000 });
  183. }
  184. // ===== 搜索和筛选功能 =====
  185. /**
  186. * 按订单名称搜索
  187. * @param name 订单名称
  188. */
  189. async searchByName(name: string) {
  190. await this.searchInput.fill(name);
  191. await this.searchButton.click();
  192. await this.page.waitForLoadState('networkidle');
  193. await this.page.waitForTimeout(1000);
  194. }
  195. /**
  196. * 打开高级筛选对话框
  197. */
  198. async openFilterDialog() {
  199. const filterButton = this.page.getByRole('button', { name: /筛选|高级筛选/ });
  200. await filterButton.click();
  201. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  202. }
  203. /**
  204. * 设置筛选条件
  205. * @param filters 筛选条件
  206. */
  207. async setFilters(filters: {
  208. status?: OrderStatus;
  209. workStatus?: WorkStatus;
  210. platformId?: number;
  211. platformName?: string;
  212. companyId?: number;
  213. companyName?: string;
  214. channelId?: number;
  215. channelName?: string;
  216. dateRange?: { start?: string; end?: string };
  217. }) {
  218. // 订单状态筛选
  219. if (filters.status) {
  220. const statusFilter = this.page.getByLabel(/订单状态/);
  221. await statusFilter.click();
  222. const statusLabel = ORDER_STATUS_LABELS[filters.status];
  223. await this.page.getByRole('option', { name: statusLabel }).click();
  224. }
  225. // 工作状态筛选
  226. if (filters.workStatus) {
  227. const workStatusFilter = this.page.getByLabel(/工作状态/);
  228. await workStatusFilter.click();
  229. const workStatusLabel = WORK_STATUS_LABELS[filters.workStatus];
  230. await this.page.getByRole('option', { name: workStatusLabel }).click();
  231. }
  232. // 平台筛选
  233. if (filters.platformName) {
  234. await selectRadixOption(this.page, '平台', filters.platformName);
  235. }
  236. // 公司筛选
  237. if (filters.companyName) {
  238. await selectRadixOption(this.page, '公司', filters.companyName);
  239. }
  240. // 渠道筛选
  241. if (filters.channelName) {
  242. await selectRadixOption(this.page, '渠道', filters.channelName);
  243. }
  244. // 日期范围筛选
  245. if (filters.dateRange) {
  246. if (filters.dateRange.start) {
  247. const startDateInput = this.page.getByLabel(/开始日期|起始日期/);
  248. await startDateInput.fill(filters.dateRange.start);
  249. }
  250. if (filters.dateRange.end) {
  251. const endDateInput = this.page.getByLabel(/结束日期|截止日期/);
  252. await endDateInput.fill(filters.dateRange.end);
  253. }
  254. }
  255. }
  256. /**
  257. * 应用筛选条件
  258. */
  259. async applyFilters() {
  260. const applyButton = this.page.getByRole('button', { name: /应用|确定|筛选/ });
  261. await applyButton.click();
  262. await this.page.waitForLoadState('networkidle');
  263. await this.page.waitForTimeout(1000);
  264. }
  265. /**
  266. * 清空筛选条件
  267. */
  268. async clearFilters() {
  269. const clearButton = this.page.getByRole('button', { name: /重置|清空/ });
  270. await clearButton.click();
  271. await this.page.waitForTimeout(500);
  272. }
  273. // ===== 订单 CRUD 操作 =====
  274. /**
  275. * 打开创建订单对话框
  276. */
  277. async openCreateDialog() {
  278. await this.addOrderButton.click();
  279. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  280. }
  281. /**
  282. * 打开编辑订单对话框
  283. * @param orderName 订单名称
  284. */
  285. async openEditDialog(orderName: string) {
  286. // 找到订单行并点击编辑按钮
  287. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  288. const editButton = orderRow.getByRole('button', { name: '编辑' });
  289. await editButton.click();
  290. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  291. }
  292. /**
  293. * 打开删除确认对话框
  294. * @param orderName 订单名称
  295. */
  296. async openDeleteDialog(orderName: string) {
  297. // 找到订单行并点击删除按钮
  298. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  299. const deleteButton = orderRow.getByRole('button', { name: '删除' });
  300. await deleteButton.click();
  301. await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
  302. }
  303. /**
  304. * 填写订单表单
  305. * @param data 订单数据
  306. */
  307. async fillOrderForm(data: OrderData) {
  308. // 等待表单出现
  309. await this.page.waitForSelector('form', { state: 'visible', timeout: 5000 });
  310. // 填写订单名称
  311. if (data.name) {
  312. await this.page.getByLabel(/订单名称|名称/).fill(data.name);
  313. }
  314. // 填写预计开始日期
  315. if (data.expectedStartDate) {
  316. const dateInput = this.page.getByLabel(/预计开始日期|开始日期/);
  317. await dateInput.fill(data.expectedStartDate);
  318. }
  319. // 选择平台
  320. if (data.platformName) {
  321. await selectRadixOption(this.page, '平台', data.platformName);
  322. }
  323. // 选择公司
  324. if (data.companyName) {
  325. await selectRadixOption(this.page, '公司', data.companyName);
  326. }
  327. // 选择渠道
  328. if (data.channelName) {
  329. await selectRadixOption(this.page, '渠道', data.channelName);
  330. }
  331. // 选择订单状态(如果是编辑模式)
  332. if (data.status) {
  333. const statusLabel = ORDER_STATUS_LABELS[data.status];
  334. await selectRadixOption(this.page, '订单状态', statusLabel);
  335. }
  336. // 选择工作状态(如果是编辑模式)
  337. if (data.workStatus) {
  338. const workStatusLabel = WORK_STATUS_LABELS[data.workStatus];
  339. await selectRadixOption(this.page, '工作状态', workStatusLabel);
  340. }
  341. }
  342. /**
  343. * 提交表单
  344. * @returns 表单提交结果
  345. */
  346. async submitForm(): Promise<FormSubmitResult> {
  347. // 收集网络响应
  348. const responses: NetworkResponse[] = [];
  349. // 监听所有网络请求
  350. const responseHandler = async (response: Response) => {
  351. const url = response.url();
  352. // 监听订单管理相关的 API 请求
  353. if (url.includes('/orders') || url.includes('order')) {
  354. const requestBody = response.request()?.postData();
  355. const responseBody = await response.text().catch(() => '');
  356. let jsonBody = null;
  357. try {
  358. jsonBody = JSON.parse(responseBody);
  359. } catch {
  360. // 不是 JSON 响应
  361. }
  362. responses.push({
  363. url,
  364. method: response.request()?.method() ?? 'UNKNOWN',
  365. status: response.status(),
  366. ok: response.ok(),
  367. responseHeaders: await response.allHeaders().catch(() => ({})),
  368. responseBody: jsonBody || responseBody,
  369. });
  370. }
  371. };
  372. this.page.on('response', responseHandler);
  373. try {
  374. // 点击提交按钮(创建或更新)
  375. const submitButton = this.page.getByRole('button', { name: /^(创建|更新|保存)$/ });
  376. await submitButton.click();
  377. // 等待网络请求完成
  378. await this.page.waitForLoadState('networkidle', { timeout: 10000 });
  379. } finally {
  380. // 确保监听器总是被移除,防止内存泄漏
  381. this.page.off('response', responseHandler);
  382. }
  383. // 等待 Toast 消息显示
  384. await this.page.waitForTimeout(2000);
  385. // 检查 Toast 消息
  386. const errorToast = this.page.locator('[data-sonner-toast][data-type="error"]');
  387. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  388. const hasError = await errorToast.count() > 0;
  389. const hasSuccess = await successToast.count() > 0;
  390. let errorMessage: string | null = null;
  391. let successMessage: string | null = null;
  392. if (hasError) {
  393. errorMessage = await errorToast.first().textContent();
  394. }
  395. if (hasSuccess) {
  396. successMessage = await successToast.first().textContent();
  397. }
  398. return {
  399. success: hasSuccess || (!hasError && !hasSuccess),
  400. hasError,
  401. hasSuccess,
  402. errorMessage: errorMessage ?? undefined,
  403. successMessage: successMessage ?? undefined,
  404. responses,
  405. };
  406. }
  407. /**
  408. * 取消对话框
  409. */
  410. async cancelDialog() {
  411. const cancelButton = this.page.getByRole('button', { name: '取消' });
  412. await cancelButton.click();
  413. await this.waitForDialogClosed();
  414. }
  415. /**
  416. * 等待对话框关闭
  417. */
  418. async waitForDialogClosed() {
  419. const dialog = this.page.locator('[role="dialog"]');
  420. await dialog.waitFor({ state: 'hidden', timeout: 5000 })
  421. .catch(() => console.debug('对话框关闭超时,可能已经关闭'));
  422. await this.page.waitForTimeout(500);
  423. }
  424. /**
  425. * 确认删除操作
  426. */
  427. async confirmDelete() {
  428. const confirmButton = this.page.getByRole('button', { name: /^确认删除$/ });
  429. await confirmButton.click();
  430. // 等待确认对话框关闭和网络请求完成
  431. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  432. .catch(() => console.debug('删除确认对话框关闭超时'));
  433. await this.page.waitForLoadState('networkidle', { timeout: 10000 });
  434. await this.page.waitForTimeout(1000);
  435. }
  436. /**
  437. * 取消删除操作
  438. */
  439. async cancelDelete() {
  440. const cancelButton = this.page.getByRole('button', { name: '取消' }).and(
  441. this.page.locator('[role="alertdialog"]')
  442. );
  443. await cancelButton.click();
  444. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  445. .catch(() => console.debug('删除确认对话框关闭超时(取消操作)'));
  446. }
  447. /**
  448. * 验证订单是否存在
  449. * @param orderName 订单名称
  450. * @returns 订单是否存在
  451. */
  452. async orderExists(orderName: string): Promise<boolean> {
  453. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  454. return (await orderRow.count()) > 0;
  455. }
  456. // ===== 订单详情 =====
  457. /**
  458. * 打开订单详情对话框
  459. * @param orderName 订单名称
  460. */
  461. async openDetailDialog(orderName: string) {
  462. // 找到订单行并点击查看详情按钮
  463. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  464. const detailButton = orderRow.getByRole('button', { name: /详情|查看/ });
  465. await detailButton.click();
  466. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  467. }
  468. /**
  469. * 获取订单详情中的基本信息
  470. * @returns 订单基本信息
  471. */
  472. async getOrderDetailInfo(): Promise<{
  473. name?: string;
  474. status?: string;
  475. workStatus?: string;
  476. expectedStartDate?: string;
  477. platform?: string;
  478. company?: string;
  479. channel?: string;
  480. }> {
  481. const dialog = this.page.locator('[role="dialog"]');
  482. const result: Record<string, string | undefined> = {};
  483. // 订单名称 - 查找"订单名称"标签后的值
  484. const nameElement = dialog.locator('.text-muted-foreground').filter({ hasText: '订单名称' })
  485. .locator('..').locator('p,span,div').nth(1);
  486. if (await nameElement.count() > 0) {
  487. const text = await nameElement.textContent();
  488. result.name = text || undefined;
  489. }
  490. // 订单状态
  491. const statusElement = dialog.locator('.text-muted-foreground').filter({ hasText: '订单状态' })
  492. .locator('..').locator('p,span,div').nth(1);
  493. if (await statusElement.count() > 0) {
  494. const text = await statusElement.textContent();
  495. result.status = text || undefined;
  496. }
  497. // 工作状态
  498. const workStatusElement = dialog.locator('.text-muted-foreground').filter({ hasText: '工作状态' })
  499. .locator('..').locator('p,span,div').nth(1);
  500. if (await workStatusElement.count() > 0) {
  501. const text = await workStatusElement.textContent();
  502. result.workStatus = text || undefined;
  503. }
  504. // 预计开始日期
  505. const startDateElement = dialog.locator('.text-muted-foreground').filter({ hasText: /预计开始日期|开始日期/ })
  506. .locator('..').locator('p,span,div').nth(1);
  507. if (await startDateElement.count() > 0) {
  508. const text = await startDateElement.textContent();
  509. result.expectedStartDate = text || undefined;
  510. }
  511. // 平台
  512. const platformElement = dialog.locator('.text-muted-foreground').filter({ hasText: '平台' })
  513. .locator('..').locator('p,span,div').nth(1);
  514. if (await platformElement.count() > 0) {
  515. const text = await platformElement.textContent();
  516. result.platform = text || undefined;
  517. }
  518. // 公司
  519. const companyElement = dialog.locator('.text-muted-foreground').filter({ hasText: '公司' })
  520. .locator('..').locator('p,span,div').nth(1);
  521. if (await companyElement.count() > 0) {
  522. const text = await companyElement.textContent();
  523. result.company = text || undefined;
  524. }
  525. // 渠道
  526. const channelElement = dialog.locator('.text-muted-foreground').filter({ hasText: '渠道' })
  527. .locator('..').locator('p,span,div').nth(1);
  528. if (await channelElement.count() > 0) {
  529. const text = await channelElement.textContent();
  530. result.channel = text || undefined;
  531. }
  532. return result;
  533. }
  534. // ===== 人员关联管理 =====
  535. /**
  536. * 打开人员管理对话框
  537. *
  538. * **使用场景:**
  539. * - **从订单列表页打开**: 传入 `orderName` 参数,方法会先找到对应订单行,再点击人员管理按钮
  540. * - **从订单详情页打开**: 不传参数,方法会直接点击页面中的人员管理按钮
  541. *
  542. * @param orderName 订单名称(可选)。从列表页打开时需要传入,从详情页打开时不传
  543. *
  544. * @example
  545. * ```typescript
  546. * // 从订单列表页打开
  547. * await orderPage.openPersonManagementDialog('测试订单');
  548. *
  549. * // 从订单详情页打开
  550. * await orderPage.openDetailDialog('测试订单');
  551. * await orderPage.openPersonManagementDialog();
  552. * ```
  553. */
  554. async openPersonManagementDialog(orderName?: string) {
  555. // 如果提供了订单名称,先找到对应的订单行
  556. if (orderName) {
  557. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  558. const personButton = orderRow.getByRole('button', { name: /人员|员工/ });
  559. await personButton.click();
  560. } else {
  561. // 如果在详情页,直接点击人员管理按钮
  562. const personButton = this.page.getByRole('button', { name: /人员管理|添加人员/ });
  563. await personButton.click();
  564. }
  565. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  566. }
  567. /**
  568. * 添加人员到订单
  569. * @param personData 人员数据
  570. */
  571. async addPersonToOrder(personData: OrderPersonData) {
  572. // 点击添加人员按钮
  573. const addButton = this.page.getByRole('button', { name: /添加人员|新增人员/ });
  574. await addButton.click();
  575. await this.page.waitForTimeout(300);
  576. // 选择残疾人
  577. if (personData.disabledPersonName) {
  578. await selectRadixOption(this.page, '残疾人|选择残疾人', personData.disabledPersonName);
  579. }
  580. // 填写入职日期
  581. if (personData.hireDate) {
  582. const hireDateInput = this.page.getByLabel(/入职日期/);
  583. await hireDateInput.fill(personData.hireDate);
  584. }
  585. // 填写薪资
  586. if (personData.salary !== undefined) {
  587. const salaryInput = this.page.getByLabel(/薪资|工资/);
  588. await salaryInput.fill(String(personData.salary));
  589. }
  590. // 选择工作状态
  591. if (personData.workStatus) {
  592. const workStatusLabel = WORK_STATUS_LABELS[personData.workStatus];
  593. await selectRadixOption(this.page, '工作状态', workStatusLabel);
  594. }
  595. // 提交
  596. const submitButton = this.page.getByRole('button', { name: /^(添加|确定|保存)$/ });
  597. await submitButton.click();
  598. await this.page.waitForLoadState('networkidle');
  599. await this.page.waitForTimeout(1000);
  600. }
  601. /**
  602. * 修改人员工作状态
  603. * @param personName 人员姓名
  604. * @param newStatus 新的工作状态
  605. */
  606. async updatePersonWorkStatus(personName: string, newStatus: WorkStatus) {
  607. // 找到人员行
  608. const personRow = this.page.locator('[role="dialog"]').locator('table tbody tr').filter({ hasText: personName });
  609. // 点击编辑工作状态按钮
  610. const editButton = personRow.getByRole('button', { name: /编辑|修改/ });
  611. await editButton.click();
  612. await this.page.waitForTimeout(300);
  613. // 选择新的工作状态
  614. const workStatusLabel = WORK_STATUS_LABELS[newStatus];
  615. await selectRadixOption(this.page, '工作状态', workStatusLabel);
  616. // 提交
  617. const submitButton = this.page.getByRole('button', { name: /^(更新|保存|确定)$/ });
  618. await submitButton.click();
  619. await this.page.waitForLoadState('networkidle');
  620. await this.page.waitForTimeout(1000);
  621. }
  622. // ===== 附件管理 =====
  623. /**
  624. * 打开添加附件对话框
  625. */
  626. async openAddAttachmentDialog() {
  627. const attachmentButton = this.page.getByRole('button', { name: /添加附件|上传附件/ });
  628. await attachmentButton.click();
  629. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  630. }
  631. /**
  632. * 上传附件
  633. * @param personName 人员姓名
  634. * @param fileName 文件名
  635. * @param mimeType 文件类型(默认为 image/jpeg)
  636. */
  637. async uploadAttachment(personName: string, fileName: string, mimeType: string = 'image/jpeg') {
  638. // 选择订单人员
  639. const personSelect = this.page.getByLabel(/选择人员|订单人员/);
  640. await personSelect.click();
  641. await this.page.getByRole('option', { name: personName }).click();
  642. // 查找文件上传输入框
  643. const fileInput = this.page.locator('input[type="file"]');
  644. await fileInput.setInputFiles({
  645. name: fileName,
  646. mimeType,
  647. buffer: Buffer.from(`fake ${fileName} content`),
  648. });
  649. // 等待上传处理
  650. await this.page.waitForTimeout(500);
  651. // 提交
  652. const submitButton = this.page.getByRole('button', { name: /^(上传|确定|保存)$/ });
  653. await submitButton.click();
  654. await this.page.waitForLoadState('networkidle');
  655. await this.page.waitForTimeout(1000);
  656. }
  657. // ===== 高级操作 =====
  658. /**
  659. * 创建订单(完整流程)
  660. * @param data 订单数据
  661. * @returns 表单提交结果
  662. */
  663. async createOrder(data: OrderData): Promise<FormSubmitResult> {
  664. await this.openCreateDialog();
  665. await this.fillOrderForm(data);
  666. const result = await this.submitForm();
  667. await this.waitForDialogClosed();
  668. return result;
  669. }
  670. /**
  671. * 编辑订单(完整流程)
  672. * @param orderName 订单名称
  673. * @param data 更新的订单数据
  674. * @returns 表单提交结果
  675. */
  676. async editOrder(orderName: string, data: OrderData): Promise<FormSubmitResult> {
  677. await this.openEditDialog(orderName);
  678. await this.fillOrderForm(data);
  679. const result = await this.submitForm();
  680. await this.waitForDialogClosed();
  681. return result;
  682. }
  683. /**
  684. * 删除订单(完整流程)
  685. * @param orderName 订单名称
  686. * @returns 是否成功删除
  687. */
  688. async deleteOrder(orderName: string): Promise<boolean> {
  689. await this.openDeleteDialog(orderName);
  690. await this.confirmDelete();
  691. // 等待并检查 Toast 消息
  692. await this.page.waitForTimeout(1000);
  693. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  694. const hasSuccess = await successToast.count() > 0;
  695. return hasSuccess;
  696. }
  697. }