docs: 同步汇账最新功能,新增智能查数、账单关系等文档
对照 ledgerly 自文档上次更新以来的全部改动,补充新功能文档、修正品牌与版本残留,并新增下载与安装页;配图已重新截取并上传 OSS,本地截图目录不纳入版本管理。 变更内容: - 新增「智能查数」「账单关系」两个使用文档页,并登记侧边栏与总览 - 设置页扩充为八个面板(账号安全、数据库实时进度、外观、导航等)并改为即时保存 - 收支记录补充关系列、批量操作、AI 搜索浮窗、可配置分页与「账单详情」弹窗 - 统计面板补充饼图/渠道图下钻与筛选双向联动;规则管理补充批量删除 - AI 能力补充智能查数与关系命名;新增下载与安装页 - 开发文档同步:AI 13 类调用、数据模型 24 张表、API、前端偏好、构建部署 - 统一「汇账 Ledgerly」品牌与「设置/日志」菜单文案,版本号对齐 0.1 - 截图脚本新增账单详情等场景并更新登记 说明: - download.md 的 OSS 下载链接保持待发布产物回填,暂不调整 - 已通过 npm run docs:build 与 npm run typecheck:scripts
This commit is contained in:
+135
-17
@@ -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, SAMPLE_IMAGE, SAMPLE_IMAGE2 } from './config.js'
|
||||
import { BASE_URL, ELEM_DIR, OUT_DIR, SAMPLE_BILL, SAMPLE_IMAGE, SAMPLE_IMAGE2 } from './config.js'
|
||||
|
||||
/** 页面就绪标志:列表页表格、设置面板或卡片 */
|
||||
const PAGE_READY = '.n-data-table, .config-card, .n-card'
|
||||
@@ -160,26 +160,59 @@ export async function captureAiRecordImage(page: Page, importRecords = true): Pr
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取「来源图片」查看弹窗(点击表格中图片记账记录的「查看(N)」)。
|
||||
* 截取「账单详情」弹窗(含基本信息、标签、账单关系、来源图片与原始数据)。
|
||||
*
|
||||
* 优先选择「有来源图片、无账单关系」的记录,使弹窗聚焦来源图片与原始数据。
|
||||
*
|
||||
* @param page Page 实例
|
||||
*/
|
||||
export async function captureSourceImages(page: Page): Promise<void> {
|
||||
export async function captureBillDetail(page: Page): Promise<void> {
|
||||
try {
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
await page.setViewportSize({ width: 1440, height: 1200 })
|
||||
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')
|
||||
const rows = page.locator('.n-data-table-tbody tr')
|
||||
const count = await rows.count()
|
||||
for (let i = 0; i < count; i++) {
|
||||
const detailBtn = rows.nth(i).locator('button', { hasText: '详情' }).first()
|
||||
if ((await detailBtn.count()) === 0) continue
|
||||
await detailBtn.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: 15000 }).catch(() => undefined)
|
||||
await page.waitForTimeout(800)
|
||||
|
||||
const imageSection = modal.locator('.detail-section', { hasText: '来源图片' }).first()
|
||||
const imgCount = await imageSection.locator('img').count()
|
||||
const relationSection = modal.locator('.detail-section', { hasText: '账单关系' }).first()
|
||||
const relationText = (await relationSection.innerText().catch(() => '')) || ''
|
||||
const noRelation = relationText.includes('暂无关联账单')
|
||||
|
||||
if (imgCount > 0 && noRelation) {
|
||||
// 加大视口并取消弹窗内部滚动,使来源图片与原始数据一并入镜
|
||||
await page.setViewportSize({ width: 1440, height: 2400 })
|
||||
await page.evaluate(() => {
|
||||
document
|
||||
.querySelectorAll<HTMLElement>('.n-modal, .n-modal .n-card, .n-modal .n-card__content')
|
||||
.forEach((el) => {
|
||||
el.style.maxHeight = 'none'
|
||||
el.style.overflow = 'visible'
|
||||
})
|
||||
})
|
||||
await page.waitForTimeout(500)
|
||||
await modal.screenshot({ path: resolve(ELEM_DIR, 'bill-detail.png') })
|
||||
console.log('[OK] modal/bill-detail')
|
||||
return
|
||||
}
|
||||
|
||||
// 不满足条件则关闭弹窗,继续查看下一行
|
||||
await page.locator('.n-modal .n-base-close').first().click().catch(() => undefined)
|
||||
await page.keyboard.press('Escape').catch(() => undefined)
|
||||
await page.waitForTimeout(400)
|
||||
}
|
||||
throw new Error('未找到「有来源图片且无账单关系」的记录')
|
||||
} catch (err) {
|
||||
console.log(`[FAIL] modal/source-images: ${err}`)
|
||||
console.log(`[FAIL] modal/bill-detail: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,12 +251,12 @@ export async function captureTrendJump(page: Page): Promise<void> {
|
||||
await waitPage(page, { selector: 'canvas' })
|
||||
// 切到「近12月 + 按月」,柱子更宽便于稳定点中;同时清空收支记录筛选偏好,避免旧条件干扰
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('finance.dashboard.prefs', JSON.stringify({
|
||||
localStorage.setItem('ledgerly.dashboard.prefs', JSON.stringify({
|
||||
rangeType: '12month', granularity: 'month', filterStatsScope: 'INCLUDED',
|
||||
filterType: [], filterCategory: [], filterChannel: [], filterTag: [],
|
||||
filterMinAmount: null, filterMaxAmount: null, filterRemark: '',
|
||||
}))
|
||||
localStorage.removeItem('finance.transaction.filterPrefs')
|
||||
localStorage.removeItem('ledgerly.transaction.filterPrefs')
|
||||
})
|
||||
await page.reload()
|
||||
await waitPage(page, { selector: 'canvas' })
|
||||
@@ -255,10 +288,12 @@ export async function captureTrendJump(page: Page): Promise<void> {
|
||||
|
||||
/** 系统设置面板:输出文件名与面板标题 */
|
||||
const SETTINGS_PANELS: Array<[string, string]> = [
|
||||
['panel-account-security', '账号安全'],
|
||||
['panel-database', '数据库配置'],
|
||||
['panel-registration', '注册设置'],
|
||||
['panel-bill-source-stats', '账单来源统计默认值'],
|
||||
['panel-display', '外观设置'],
|
||||
['panel-nav-menu', '导航设置'],
|
||||
['panel-ai', 'AI 模型配置'],
|
||||
['panel-notification', '定时通知'],
|
||||
]
|
||||
@@ -348,6 +383,89 @@ export async function captureSettingsPanels(page: Page, only?: string[]): Promis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取 AI 查询浮窗(统计面板顶部的「AI 搜索」入口)。
|
||||
*
|
||||
* @param page Page 实例
|
||||
*/
|
||||
export async function captureAiQueryWindow(page: Page): Promise<void> {
|
||||
try {
|
||||
await page.setViewportSize({ width: 1440, height: 1000 })
|
||||
await page.goto(BASE_URL + '/dashboard')
|
||||
await waitPage(page, { selector: 'canvas' })
|
||||
if (!(await clickText(page, 'AI 搜索'))) throw new Error('未找到「AI 搜索」按钮')
|
||||
const panel = page.locator('.floating-panel').first()
|
||||
await panel.waitFor({ state: 'visible', timeout: 20000 })
|
||||
await page.waitForTimeout(600)
|
||||
await panel.screenshot({ path: resolve(ELEM_DIR, 'ai-query-window.png') })
|
||||
console.log('[OK] modal/ai-query-window')
|
||||
} catch (err) {
|
||||
console.log(`[FAIL] modal/ai-query-window: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取账单关系:关系查看面板(含关系图)与新建关系弹窗。
|
||||
*
|
||||
* @param page Page 实例
|
||||
*/
|
||||
export async function captureRelations(page: Page): Promise<void> {
|
||||
try {
|
||||
await page.setViewportSize({ width: 1440, height: 1100 })
|
||||
await page.goto(BASE_URL + '/transactions')
|
||||
await waitPage(page, { selector: '.n-data-table-tbody tr' })
|
||||
const relBtn = page.locator('button', { hasText: /个关系/ }).first()
|
||||
await relBtn.waitFor({ state: 'visible', timeout: 20000 })
|
||||
await relBtn.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, 'relation-panel.png') })
|
||||
console.log('[OK] modal/relation-panel')
|
||||
|
||||
const createBtn = modal.locator('button', { hasText: '新建关系' }).first()
|
||||
await createBtn.waitFor({ state: 'visible', timeout: 15000 })
|
||||
await createBtn.evaluate((el) => (el as HTMLElement).click())
|
||||
const editor = page.locator('.n-modal', { hasText: '新建账单关系' }).first()
|
||||
await editor.waitFor({ state: 'visible', timeout: 20000 })
|
||||
await page.waitForTimeout(800)
|
||||
await editor.screenshot({ path: resolve(ELEM_DIR, 'relation-editor.png') })
|
||||
console.log('[OK] modal/relation-editor')
|
||||
} catch (err) {
|
||||
console.log(`[FAIL] modal/relations: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取智能查数整页(先执行一次查询再截图)。
|
||||
*
|
||||
* @param page Page 实例
|
||||
*/
|
||||
export async function captureSqlQuery(page: Page): Promise<void> {
|
||||
try {
|
||||
await page.setViewportSize({ width: 1440, height: 1180 })
|
||||
await page.goto(BASE_URL + '/smart-query')
|
||||
await waitPage(page, { selector: '.n-card' })
|
||||
const input = page.locator('textarea').first()
|
||||
await input.waitFor({ state: 'visible', timeout: 20000 })
|
||||
await input.fill('按分类统计今年的支出')
|
||||
await Promise.all([
|
||||
page.waitForResponse(
|
||||
(r) => r.url().includes('/ai/sql') && r.request().method() === 'POST',
|
||||
{ timeout: 130000 },
|
||||
),
|
||||
page.locator('button', { hasText: '查询' }).first().click(),
|
||||
])
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => undefined)
|
||||
await page.waitForTimeout(1500)
|
||||
await page.screenshot({ path: resolve(OUT_DIR, 'sql-query.png') })
|
||||
console.log('[OK] page/sql-query')
|
||||
} catch (err) {
|
||||
console.log(`[FAIL] page/sql-query: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const browser = await launchBrowser()
|
||||
try {
|
||||
@@ -364,7 +482,7 @@ async function main(): Promise<void> {
|
||||
await captureEditRecord(page)
|
||||
await captureAiRule(page)
|
||||
await captureAiRecordImage(page)
|
||||
await captureSourceImages(page)
|
||||
await captureBillDetail(page)
|
||||
await captureChannelAi(page)
|
||||
await captureSettingsPanels(page)
|
||||
await captureBillParse(page)
|
||||
|
||||
@@ -10,6 +10,7 @@ const PAGES: Array<[string, string, number, number, string]> = [
|
||||
['login', '/login', 1440, 900, '.n-card'],
|
||||
['register', '/register', 1440, 900, '.n-card'],
|
||||
['dashboard', '/dashboard', 1440, 1280, 'canvas'],
|
||||
['sql-query', '/smart-query', 1440, 1180, '.n-card'],
|
||||
['transactions', '/transactions', 1720, 1200, '.n-data-table-tbody tr'],
|
||||
['categories', '/categories', 1440, 1100, '.n-data-table-tbody tr'],
|
||||
['tags', '/tags', 1440, 1100, '.n-data-table-tbody tr'],
|
||||
@@ -19,7 +20,7 @@ const PAGES: Array<[string, string, number, number, string]> = [
|
||||
['email-bills-sender-rules', '/email-bills/senderRules', 1440, 1000, '.n-data-table-tbody tr'],
|
||||
['email-bills-messages', '/email-bills/messages', 1440, 1000, '.n-data-table-tbody tr'],
|
||||
['bill-import', '/bill-import', 1440, 1000, '.n-card'],
|
||||
['settings', '/settings', 1440, 1320, '.config-card'],
|
||||
['settings', '/settings', 1440, 1900, '.config-card'],
|
||||
['ai-usage', '/ai-usage', 1440, 1000, '.n-card'],
|
||||
['logs-http', '/logs/http', 1440, 1000, '.n-data-table-tbody tr'],
|
||||
['logs-login', '/logs/login', 1440, 1000, '.n-data-table-tbody tr'],
|
||||
|
||||
@@ -164,6 +164,9 @@ async function assumeRole(config: OssConfig): Promise<StsCredentials> {
|
||||
accessKeyId: config.accessKeyId,
|
||||
accessKeySecret: config.accessKeySecret,
|
||||
endpoint: STS_ENDPOINT,
|
||||
// 默认连接超时较短,网络抖动时易 ConnectTimeout,这里放宽
|
||||
connectTimeout: 20000,
|
||||
readTimeout: 20000,
|
||||
}),
|
||||
)
|
||||
const request = new AssumeRoleRequest({
|
||||
|
||||
Reference in New Issue
Block a user