feat: 补充图片记账、交易渠道管理与前端偏好文档
- 新增「交易渠道管理」指南与「前端用户偏好」开发文档,并加入侧边栏导航 - 图片记账:补充多图上传、实时识别进度、结果勾选导入与「来源图片」回看,AI 配置新增视觉模型说明 - 数据模型补充 t_channel、t_ai_config/t_ai_image 等表与「类型+金额+时间」去重键说明,表数更新为 21 张 - API 文档补充 /api/channel、/api/transaction/batch、/api/ai/record/image(/stream)、/api/ai/image、/api/ai/channel 等接口 - 截图脚本新增渠道页、图片记账、来源图片与渠道 AI 辅助截图,并放宽导航超时
This commit is contained in:
@@ -3,7 +3,7 @@ import { resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import type { Page } from 'playwright-core'
|
||||
import { clickText, injectSession, launchBrowser, login, newContext, waitPage } from './browser.js'
|
||||
import { BASE_URL, ELEM_DIR, SAMPLE_BILL } from './config.js'
|
||||
import { BASE_URL, ELEM_DIR, SAMPLE_BILL, SAMPLE_IMAGE, SAMPLE_IMAGE2 } from './config.js'
|
||||
|
||||
/** 页面就绪标志:列表页表格、设置面板或卡片 */
|
||||
const PAGE_READY = '.n-data-table, .config-card, .n-card'
|
||||
@@ -26,6 +26,7 @@ const MODAL_SCENES: Array<[string, string, string]> = [
|
||||
['add-rule', '/rules', '新增规则'],
|
||||
['add-email-config', '/email-bills/configs', '新增'],
|
||||
['add-channel', '/settings', '添加渠道'],
|
||||
['channel-create', '/channels', '新建渠道'],
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -110,6 +111,101 @@ export async function captureBillParse(page: Page): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取「图片记账」:多图上传 Tab 与识别结果弹窗。
|
||||
*
|
||||
* @param page Page 实例
|
||||
* @param importRecords 识别后是否导入勾选记录(默认 true,便于后续截取来源图片)
|
||||
*/
|
||||
export async function captureAiRecordImage(page: Page, importRecords = true): Promise<void> {
|
||||
try {
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
await page.goto(BASE_URL + '/transactions')
|
||||
await waitPage(page, { selector: PAGE_READY })
|
||||
if (!(await clickText(page, '智能记账'))) throw new Error('未找到「智能记账」按钮')
|
||||
const modal = page.locator('.n-modal').first()
|
||||
await modal.waitFor({ state: 'visible', timeout: 20000 })
|
||||
|
||||
// 切到「图片记账」标签页,上传两张虚构账单截图后截取上传界面
|
||||
await modal.locator('.n-tabs-tab', { hasText: '图片记账' }).first().click()
|
||||
await page.waitForTimeout(600)
|
||||
await modal.locator("input[type='file']").setInputFiles([SAMPLE_IMAGE, SAMPLE_IMAGE2])
|
||||
await page.waitForTimeout(600)
|
||||
await modal.screenshot({ path: resolve(ELEM_DIR, 'ai-record-image.png') })
|
||||
console.log('[OK] modal/ai-record-image')
|
||||
|
||||
const recognize = modal.locator('button', { hasText: '识别图片' }).first()
|
||||
await recognize.evaluate((el) => (el as HTMLElement).click())
|
||||
|
||||
// 结果弹窗出现(标题「图片记账结果」)且表格有数据
|
||||
const resultModal = page.locator('.n-modal', { hasText: '图片记账结果' }).first()
|
||||
await resultModal.waitFor({ state: 'visible', timeout: 180000 })
|
||||
await resultModal.locator('.n-data-table-tbody tr').first().waitFor({ state: 'visible', timeout: 180000 })
|
||||
await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => undefined)
|
||||
await page.waitForTimeout(800)
|
||||
await resultModal.screenshot({ path: resolve(ELEM_DIR, 'ai-record-result.png') })
|
||||
console.log('[OK] modal/ai-record-result')
|
||||
|
||||
// 导入勾选记录,便于后续「来源图片」截图(去重键保证重复执行不会产生新记录)
|
||||
if (importRecords) {
|
||||
const importBtn = resultModal.locator('button', { hasText: '导入勾选' }).first()
|
||||
await importBtn.evaluate((el) => (el as HTMLElement).click())
|
||||
await resultModal.waitFor({ state: 'hidden', timeout: 60000 }).catch(() => undefined)
|
||||
await page.waitForTimeout(1000)
|
||||
console.log('[OK] modal/ai-record-import')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`[FAIL] modal/ai-record-image: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取「来源图片」查看弹窗(点击表格中图片记账记录的「查看(N)」)。
|
||||
*
|
||||
* @param page Page 实例
|
||||
*/
|
||||
export async function captureSourceImages(page: Page): Promise<void> {
|
||||
try {
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
await page.goto(BASE_URL + '/transactions')
|
||||
await waitPage(page, { selector: '.n-data-table-tbody tr' })
|
||||
const viewBtn = page.locator('button', { hasText: /查看(\d+)/ }).first()
|
||||
await viewBtn.waitFor({ state: 'visible', timeout: 20000 })
|
||||
await viewBtn.evaluate((el) => (el as HTMLElement).click())
|
||||
const modal = page.locator('.n-modal', { hasText: '来源图片' }).first()
|
||||
await modal.waitFor({ state: 'visible', timeout: 20000 })
|
||||
await page.waitForLoadState('networkidle', { timeout: 20000 }).catch(() => undefined)
|
||||
await page.waitForTimeout(1000)
|
||||
await modal.screenshot({ path: resolve(ELEM_DIR, 'source-images.png') })
|
||||
console.log('[OK] modal/source-images')
|
||||
} catch (err) {
|
||||
console.log(`[FAIL] modal/source-images: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取编辑记录弹窗中「交易渠道」的 AI 推荐与新建按钮。
|
||||
*
|
||||
* @param page Page 实例
|
||||
*/
|
||||
export async function captureChannelAi(page: Page): Promise<void> {
|
||||
try {
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
await page.goto(BASE_URL + '/transactions')
|
||||
await waitPage(page, { selector: PAGE_READY })
|
||||
const editBtn = page.locator('tbody tr').first().locator('button', { hasText: '编辑' }).first()
|
||||
await editBtn.waitFor({ state: 'visible', timeout: 20000 })
|
||||
await editBtn.click()
|
||||
const modal = page.locator('.n-modal').first()
|
||||
await modal.waitFor({ state: 'visible', timeout: 20000 })
|
||||
await page.waitForTimeout(800)
|
||||
await modal.screenshot({ path: resolve(ELEM_DIR, 'ai-channel-suggest.png') })
|
||||
console.log('[OK] modal/ai-channel-suggest')
|
||||
} catch (err) {
|
||||
console.log(`[FAIL] modal/ai-channel-suggest: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取「点击趋势柱状图跳转收支记录并自动带入筛选条件」的筛选卡片。
|
||||
*
|
||||
@@ -228,15 +324,17 @@ export async function captureAiRule(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取系统设置的六个配置面板。
|
||||
* 截取系统设置的配置面板。
|
||||
*
|
||||
* @param page Page 实例
|
||||
* @param only 仅截取指定标题的面板(可选,缺省截取全部)
|
||||
*/
|
||||
export async function captureSettingsPanels(page: Page): Promise<void> {
|
||||
export async function captureSettingsPanels(page: Page, only?: string[]): Promise<void> {
|
||||
await page.setViewportSize({ width: 1440, height: 1200 })
|
||||
await page.goto(BASE_URL + '/settings')
|
||||
await waitPage(page, { selector: PAGE_READY })
|
||||
for (const [name, title] of SETTINGS_PANELS) {
|
||||
const panels = only ? SETTINGS_PANELS.filter(([, title]) => only.includes(title)) : SETTINGS_PANELS
|
||||
for (const [name, title] of panels) {
|
||||
try {
|
||||
const card = page.locator('.config-card', { hasText: title }).first()
|
||||
await card.waitFor({ state: 'visible', timeout: 20000 })
|
||||
@@ -265,6 +363,9 @@ async function main(): Promise<void> {
|
||||
await captureModals(page)
|
||||
await captureEditRecord(page)
|
||||
await captureAiRule(page)
|
||||
await captureAiRecordImage(page)
|
||||
await captureSourceImages(page)
|
||||
await captureChannelAi(page)
|
||||
await captureSettingsPanels(page)
|
||||
await captureBillParse(page)
|
||||
await captureTrendJump(page)
|
||||
|
||||
Reference in New Issue
Block a user