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:
2026-09-21 22:19:55 +08:00
parent a4ae7ac979
commit d2e4a51f2b
20 changed files with 557 additions and 40 deletions
+105 -4
View File
@@ -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)