docs: add complete login and existing stock data download workflow

- Add Mermaid sequence diagram covering login to download completion
- Document 5 phases: Login (4 steps), App selection (2 steps), Query scheme (2 steps), Download execution (2 steps), Data waiting (1 step)
- Include element loading wait mechanism with TypeScript implementation
- Add HOVER trigger warning for row operation dropdown (emphasized by user)
- Add complete outerHTML reference for all UI elements
- Document forced login popup handling
- Add element selector reference table
This commit is contained in:
Misaka_Company
2026-04-09 15:02:33 +08:00
parent 440b74d09a
commit 08a145af81

View File

@@ -0,0 +1,390 @@
# 登录及现存量数据下载完整流程
本文档使用 Mermaid 流程图呈现从**登录系统**到**完成现存量数据下载**的全过程。
> **⚠️ 重要提示**
>
> 1. **每个操作前必须等待目标元素加载完成** —— 网页反应速度有限,不可假设点击后立即可以操作下一步
> 2. **下载步骤 9 只能用 HOVER 触发,点击无效** —— 用户特别强调 (4 个感叹号)
> 3. **条件分支以用户实际观察为准** —— 不推测原因,只呈现现象
> 4. **登录流程包含强制登录弹窗处理** —— 系统可能弹出"确定"确认框
## 流程总览
```mermaid
sequenceDiagram
participant User as 用户
participant Page as 页面
participant System as 系统
participant HistoryPanel as 历史数据侧边栏
Note over User,HistoryPanel: 阶段 0: 登录系统
Page->>Page: 导航到登录 URL
Page->>Page: 等待#forwardFrame iframe 加载完成
User->>Page: 1. 填写用户名
Page->>Page: 等待密码框加载完成
User->>Page: 2. 填写密码
Page->>Page: 等待登录按钮加载完成
User->>Page: 3. 点击登录按钮
alt 出现强制登录确认框
Page->>Page: 弹出"确定"确认框
User->>Page: 4. 点击"确定"按钮
Note over User,HistoryPanel: 强制登录模式
else 无确认框
Note over User,HistoryPanel: 正常登录模式
end
Page->>Page: 等待登录完成
Page->>Page: 进入系统主页
Note over User,HistoryPanel: 阶段 1: 应用选择
Page->>Page: 等待应用菜单图标加载完成
User->>Page: 5. 点击应用菜单图标
Page->>Page: 展开应用列表
Page->>Page: 等待"现存量"应用项加载完成
User->>Page: 6. 点击"现存量"应用
Page->>Page: 打开新标签页
Note over User,HistoryPanel: 阶段 2: 查询方案选择
Page->>Page: 等待行操作下拉图标加载完成
User->>Page: 7. 点击行操作下拉图标
Page->>Page: 弹出查询方案列表
Page->>Page: 等待"现存量 - 总量"选项加载完成
User->>Page: 8. 点击"现存量 - 总量"
alt 出现"正在加载,请耐心等待"提示
Page->>Page: 显示全屏加载提示
Page->>Page: 等待加载完成
Page->>Page: 显示查询结果
else 页面无提示,仅短暂卡顿 (1-3 秒)
Page->>Page: 短暂卡顿
Page->>Page: 显示查询结果
end
Note over User,HistoryPanel: 阶段 3: 执行下载
Page->>Page: 等待查询结果数据加载完成
Page->>Page: 等待行操作下拉图标 (打印图标) 加载完成
rect rgb(255, 240, 200)
Note over User,Page: ⚠️ 只能使用 **HOVER**,点击无法触发!
User->>Page: 9. HOVER 触发行操作下拉图标
end
Page->>Page: 弹出操作菜单
Page->>Page: 等待"输出 xlsx 文件"选项加载完成
User->>Page: 10. 点击"输出 xlsx 文件"
alt 弹出确认对话框
Page->>Page: 显示确认对话框
Page->>Page: 记录点击"继续"的时间戳 T
User->>Page: 点击"继续"按钮
Note over User,HistoryPanel: 阶段 4: 等待数据准备
User->>HistoryPanel: 11. 点击"历史数据"图标
HistoryPanel->>HistoryPanel: 打开侧边栏
Page->>Page: 等待"实时分享"标签加载完成
User->>HistoryPanel: 点击"实时分享"标签
HistoryPanel->>HistoryPanel: 显示数据列表
loop 循环检查直到找到目标数据
HistoryPanel->>HistoryPanel: 获取所有记录的时间戳
HistoryPanel->>HistoryPanel: 检查:存在时间戳 > T 的记录?
alt 存在符合条件的记录
HistoryPanel->>HistoryPanel: 找到目标数据
else 不存在符合条件的记录
User->>HistoryPanel: 关闭侧边栏
Note over User,HistoryPanel: 等待片刻 (如 5 秒)
User->>HistoryPanel: 再次点击"历史数据"图标
end
end
Page->>Page: 等待下载图标加载完成
User->>HistoryPanel: 点击下载图标
HistoryPanel->>User: 触发下载
else 无对话框,直接下载
Page->>User: 直接触发下载
end
Note over User,HistoryPanel: ✅ 下载完成
```
## 详细步骤说明
### 阶段 0: 登录系统
| 步骤 | 操作 | 前置条件 | 元素选择器 / OuterHTML |
|------|------|----------|----------------------|
| 1 | 填写用户名 | 登录页面 #forwardFrame iframe 已加载完成 | `main_frame.get_by_role("textbox", name="用户名").fill(username)` |
| 2 | 填写密码 | 用户名已填写完成 | `main_frame.get_by_role("textbox", name="密码").fill(password)` |
| 3 | 点击登录按钮 | 密码已填写完成 | `main_frame.get_by_role("button", name="登录").click()` |
| 4 | 处理强制登录确认框 (如出现) | 登录按钮已点击 | `main_frame.get_by_role("button", name="确定").click()` |
**登录流程说明:**
1. **导航到登录页面** - 使用完整 URL
```python
url = f"{base_url.rstrip('/')}/yonbip/resources/uap/rbac/login/main/index.html"
```
2. **定位 iframe** - 所有登录表单元素都在 `#forwardFrame` iframe 内:
```python
main_frame = page.locator("#forwardFrame").content_frame
```
3. **填写凭据** - 使用 Playwright 的 role 定位器:
```python
main_frame.get_by_role("textbox", name="用户名").fill(username)
main_frame.get_by_role("textbox", name="密码").fill(password)
```
4. **点击登录** - 登录按钮:
```python
main_frame.get_by_role("button", name="登录").click()
```
5. **处理强制登录弹窗** - 系统可能弹出确认框:
```python
confirm_btn = main_frame.get_by_role("button", name="确定")
if confirm_btn.count() > 0:
confirm_btn.click() # 强制登录模式
else:
pass # 正常登录模式
```
6. **等待登录完成** - 登录后进入系统主页,准备进行下一步操作
---
### 阶段 1: 应用选择
| 步骤 | 操作 | 前置条件 | 元素 OuterHTML (完整) |
|------|------|----------|----------------------|
| 5 | 点击应用菜单图标 | 登录后的主页已加载完成 | `<div class="nc-workbench-icon" data-step="1" data-intro=" &lt;img src=images/novice-guide-allapps.gif alt=&quot;&quot; class='novice-guide-allapps' /&gt; &lt;p&gt;应用菜单&lt;/p&gt; &lt;div class='span-box'&gt; &lt;span&gt;展示全部有权限的应用。在"我的应用"中,可查看"我的收藏"和"最近访问"的应用。&lt;/span&gt; &lt;/div&gt; &lt;i class='iconfont icon-guanbi2 guide-close' onclick=&quot;handleWorkbenchIntro('close')&quot;&gt;&lt;/i&gt; "><i style="background-image: url(&quot;images/logo2.svg&quot;);"></i></div>` |
| 6 | 点击"现存量"应用 | 应用列表已展开完成 | `<div class="item-app" open-type="tab" grp-index="0" item-index="5" title="现存量" style="min-width: 255px;">现存量<div class="icon-content"><i class="iconfont icon-xinyeqiandakai app-open" grp-index="0" item-index="5" open-type="newtab" date-for-wui-tooltip="wui-tooltip-8makk4g2uj"></i><i class="iconfont icon-shoucangdianliang" date-for-wui-tooltip="wui-tooltip-79heoa4zzh" style="color: rgb(240, 158, 68); font-size: 16px; visibility: visible;"></i></div></div>` |
### 阶段 2: 查询方案选择
| 步骤 | 操作 | 前置条件 | 元素 OuterHTML (完整) |
|------|------|----------|----------------------|
| 7 | 点击行操作下拉图标 | 现存量页面已加载完成 | `<i class="iconfont icon-hangcaozuoxiala1"></i>` |
| 8 | 点击"现存量 - 总量"方案 | 查询方案列表已弹出完成 | (列表项,通过文本"现存量 - 总量"定位) |
**步骤 8 后的两种可能情况:**
| 情况 | 现象 | OuterHTML | 处理 |
|------|------|-----------|------|
| 可能性 1 | 出现"正在加载,请耐心等待"全屏提示 | `<div class="wui-spin-backdrop wui-spin-full-screen base-loading-back-drop" style="z-index: 1900;"><div class="wui-spin-default-container "><div class="wui-spin wui-spin-default wui-spin-show-text base-loading base-loading-spin-show"><div class="wui-spin-spin wui-spin-dot wui-spin-dot-spin"><i></i><i></i><i></i><i></i></div></div><div class="wui-spin-desc">加载中...</div></div></div>` | 等待加载完成,直到该元素消失 |
| 可能性 2 | 页面无提示,仅短暂卡顿 (1-3 秒) | 无额外元素出现 | 等待 1-3 秒即可 |
### 阶段 3: 执行下载
| 步骤 | 操作 | 前置条件 | 元素 OuterHTML (完整) |
|------|------|----------|----------------------|
| 9 | **HOVER** 触发行操作下拉图标 | 查询结果数据已显示完成 | `<span class="wui-button-text-wrap"><i class="arrow iconfont icon-hangcaozuoxiala1 nc-button-area-print-icon "></i></span>` |
| 10 | 点击"输出 xlsx 文件" | 操作菜单已弹出完成 | `<li class="wui-menu-item wui-menu-item-only-child dropdown-btn-item" role="menuitem" tabindex="-1" btn-code="xlsx" date-for-wui-tooltip="wui-tooltip-cj1jzq1434" aria-disabled="false" data-menu-id="rc-menu-uuid-36646-2-xlsx"><span date-for-wui-tooltip="wui-tooltip-vvedhdkxwj"><div class="dropdown-btn-item-box"><div class="btn-item-left-wrapper"><div class="btn-item-left" btn-code="xlsx">输出 xlsx 文件</div></div></div></span></li>` |
> **⚠️ 步骤 9 特别警告**
>
> - **必须使用 HOVER 动作,点击无法正确触发!**
> - 用户用 4 个感叹号强调此点
> - 实现代码示例:
> ```typescript
> // 正确方式
> await dropdownTrigger.hover();
> await page.waitForTimeout(1000); // 等待菜单展开
>
> // ❌ 错误方式:不要使用 click()
> // await dropdownTrigger.click();
> ```
**步骤 10 后的两种可能情况:**
| 情况 | 现象 | OuterHTML | 处理 |
|------|------|-----------|------|
| 可能性 1 | 弹出确认对话框 | `<div class="wui-modal-body">当前数据较多,系统在处理完成后会发送通知消息,是否继续?</div>`<br/>`<button type="button" class="wui-button sure-button nc-button-wrapper button-primary " tabindex="-1"><span class="wui-button-text-wrap">继续</span></button>` | 点击"继续"按钮,然后进入阶段 4 |
| 可能性 2 | 无对话框 | 无额外元素出现 | 直接触发下载,流程结束 |
### 阶段 4: 等待数据准备 (仅当步骤 10 弹出确认对话框时)
**确认对话框元素:**
```html
<div class="wui-modal-resizbox modal-content-resizeWrap react-draggable">
<div class="wui-modal-content">
<div class="wui-modal-body">当前数据较多,系统在处理完成后会发送通知消息,是否继续?</div>
<div class="wui-modal-footer">
<button class="wui-button sure-button">继续</button>
<button class="wui-button cancel-button">取消</button>
</div>
</div>
</div>
```
**数据等待子流程:**
```mermaid
flowchart TD
A[点击继续按钮] --> B[记录点击时间 T<br/>格式YYYY-MM-DD HH:mm:ss]
B --> C[点击历史数据图标]
C --> D[侧边栏打开]
D --> E[点击实时分享标签]
E --> F[获取数据列表]
F --> G{存在时间戳 > T 的记录?}
G -->|是 | H[找到目标数据<br/>选择最新的一条]
G -->|否 | I[关闭侧边栏]
I --> J["等待片刻 (建议 5 秒)"]
J --> C
H --> K[点击下载图标]
K --> L[下载完成]
```
**历史数据元素结构 (完整 OuterHTML)**
```html
<li class="history nc-theme-xrow-bgc">
<p class="title sidebox-title-class" date-for-wui-tooltip="wui-tooltip-d2n8imkyvl">现存量</p>
<span class="ts sidebox-ts-class">2026-04-09 14:16:03</span>
<br>
<span class="ts sidebox-ts-class">发送人:彭强强</span>
<i class="read-icon not-read">未读</i>
<span class="icon iconfont icon-xiazai1" date-for-wui-tooltip="wui-tooltip-h6v3886qti"></span>
</li>
```
**时间判断逻辑:**
1. 记录点击"继续"按钮的时间戳 `T` (格式:`YYYY-MM-DD HH:mm:ss`)
2. 获取历史数据列表中所有记录的时间戳 (从 `<span class="ts sidebox-ts-class">` 提取)
3. 遍历检查:`record_timestamp > T`
4. 如果存在符合条件的记录 → 选择**最新的一条** (第一条通常是最新的)
5. 如果不存在 → 关闭侧边栏,等待 5 秒后重试
---
## 元素加载等待机制
**这是用户特别强调的核心机制** —— 每次操作前必须确认目标元素已加载完成。
### 等待函数实现
```typescript
/**
* 等待元素加载完成
* @param locator - Playwright locator
* @param options - 配置选项
* - timeout: 超时时间 (毫秒),默认 30000
* - state: 等待状态 ('visible' | 'attached' | 'hidden'),默认 'visible'
* - extraDelay: 额外等待时间 (毫秒),默认 500确保元素完全可交互
*/
async function waitForElement(locator: Locator, options: {
timeout?: number;
state?: 'visible' | 'attached' | 'hidden';
extraDelay?: number;
} = {}): Promise<void> {
const { timeout = 30000, state = 'visible', extraDelay = 500 } = options;
// 等待元素达到指定状态
await locator.waitFor({ state, timeout });
// 额外等待,确保元素完全可交互
if (extraDelay > 0) {
await page.waitForTimeout(extraDelay);
}
}
```
### 在主流程中的应用
每个步骤的标准操作模式:
```typescript
// 登录阶段:填写用户名
const usernameInput = main_frame.getByRole('textbox', { name: '用户名' });
await waitForElement(usernameInput);
await usernameInput.fill(username);
// 登录阶段:填写密码
const passwordInput = main_frame.getByRole('textbox', { name: '密码' });
await waitForElement(passwordInput);
await passwordInput.fill(password);
// 登录阶段:点击登录按钮
const loginButton = main_frame.getByRole('button', { name: '登录' });
await waitForElement(loginButton);
await loginButton.click();
// 登录阶段:处理强制登录确认框 (如出现)
const confirmButton = main_frame.getByRole('button', { name: '确定' });
if (await confirmButton.count() > 0) {
await confirmButton.click();
}
// 步骤 5: 点击应用菜单
const menuIcon = page.locator('.nc-workbench-icon');
await waitForElement(menuIcon);
await menuIcon.click();
// 步骤 6: 点击"现存量"应用
const currentItem = page.locator('.item-app[title="现存量"]');
await waitForElement(currentItem);
await currentItem.click();
// 步骤 9: HOVER 触发 (特殊)
const dropdownTrigger = page.locator('span.wui-button-text-wrap i.icon-hangcaozuoxiala1.nc-button-area-print-icon');
await waitForElement(dropdownTrigger);
await dropdownTrigger.hover(); // ⚠️ 必须 HOVER不能 click
await page.waitForTimeout(1000); // 等待菜单展开
// 步骤 8 后的加载等待
const loadingBackdrop = page.locator('.wui-spin-backdrop.wui-spin-full-screen');
if (await loadingBackdrop.isVisible()) {
await loadingBackdrop.waitFor({ state: 'hidden', timeout: 60000 });
} else {
await page.waitForTimeout(3000); // 无提示时等待 1-3 秒
}
```
---
## 元素选择器参考表
### 主流程元素
| 阶段 | 步骤 | 元素 | 选择器 (精简) | 完整 OuterHTML 参考 |
|------|------|------|--------------|-------------------|
| 0 (登录) | 1 | 用户名输入框 | `getByRole("textbox", name="用户名")` | 见阶段 0 表格 |
| 0 (登录) | 2 | 密码输入框 | `getByRole("textbox", name="密码")` | 见阶段 0 表格 |
| 0 (登录) | 3 | 登录按钮 | `getByRole("button", name="登录")` | 见阶段 0 表格 |
| 0 (登录) | 4 | 强制登录确认按钮 | `getByRole("button", name="确定")` | 见阶段 0 表格 |
| 1 | 5 | 应用菜单图标 | `.nc-workbench-icon` | 见阶段 1 表格 |
| 1 | 6 | "现存量"应用 | `.item-app[title="现存量"]` | 见阶段 1 表格 |
| 2 | 7 | 查询方案下拉 | `i.icon-hangcaozuoxiala1` | 见阶段 2 表格 |
| 2 | 8 | "现存量 - 总量" | 文本定位 | 见阶段 2 表格 |
| 3 | 9 | 行操作下拉 (HOVER) | `span.wui-button-text-wrap i.icon-hangcaozuoxiala1.nc-button-area-print-icon` | 见阶段 3 表格 |
| 3 | 10 | "输出 xlsx 文件" | `li.wui-menu-item[btn-code="xlsx"]` | 见阶段 3 表格 |
| 4 | 11 | "历史数据"图标 | `button.nc-button-wrapper i.icon-fenxianglishi` | - |
| 4 | 11 | "实时分享"标签 | `div[role="tab"][nodekey="current"]` | - |
| 4 | 11 | 下载图标 | `span.icon-xiazai1` | 见阶段 4 历史数据元素 |
### 弹窗元素
| 元素 | 选择器 | 用途 |
|------|--------|------|
| 加载提示 | `.wui-spin-backdrop.wui-spin-full-screen` | 等待加载完成 |
| 确认对话框 `.wui-modal-body` (含文本判断) | 识别确认对话框 |
| "继续"按钮 | `button.sure-button` | 确认继续 |
---
## 实现注意事项
1. **时间戳格式必须一致** —— 点击"继续"按钮记录的时间 T 必须与页面显示的时间戳格式相同 (`YYYY-MM-DD HH:mm:ss`)
2. **循环等待需设置最大次数** —— 避免无限循环,建议设置最大重试次数 (如 10 次)
3. **侧边栏关闭后需等待** —— 关闭侧边栏后至少等待 5 秒再重新打开,给系统处理数据的时间
4. **HOVER 后需等待菜单展开** —— HOVER 动作后至少等待 1 秒再执行下一步点击