docs(reports): 更新报表接口接入文档与实现说明,报表分析接口接入

This commit is contained in:
yelan
2026-09-02 16:51:35 +08:00
parent e52f203123
commit 612e43aeff
10 changed files with 1474 additions and 795 deletions
+106 -57
View File
@@ -1,72 +1,121 @@
import { getCampuses, getDepartments } from './organization'
import { getSigningTasks, getSigningTemplates, isSigningMockEnabled } from '@/api/workbench/signing'
import type { WorkbenchCampus } from '@/api/workbench/types'
import { unwrapApiResponse } from '@/utils/api-response'
import { request } from '@/utils/request'
import type { ApiResponse } from '@/types/common'
import type { ReportTaskData } from './types'
import type {
ReportExportJobResponseDto,
ReportExportRequestDto,
ReportFilterDto,
ReportOptionsResponseDto,
ReportOverviewResponseDto,
ReportPageResponseDto,
ReportTaskResponseDto,
ReportTasksQueryDto,
} from './types'
const REPORT_PAGE_SIZE = 200
export const isReportsMockEnabled = import.meta.env.VITE_USE_MOCK !== 'false'
export const isReportsMockEnabled = isSigningMockEnabled
function compactQuery(values: object) {
return Object.fromEntries(
Object.entries(values).filter(
([, value]) => value !== undefined && value !== null && value !== '',
),
)
}
function buildSigningApiOptions(data: ReportTaskData) {
function normalizeReportOptions(data: ReportOptionsResponseDto): ReportOptionsResponseDto {
return {
templates: data.templates,
campusNames: Object.fromEntries(
data.campuses
.filter((campus) => Boolean(campus.name.trim()))
.map((campus) => [campus.id, campus.name]),
) as Record<string, WorkbenchCampus>,
campuses: data.campuses ?? [],
departments: data.departments ?? [],
templateCategories: data.templateCategories ?? [],
}
}
async function getAllReportTasks(data: ReportTaskData) {
const options = buildSigningApiOptions(data)
const query = {
page: 1,
pageSize: REPORT_PAGE_SIZE,
status: 'all' as const,
dateRange: 'all' as const,
campus: 'all' as const,
method: 'all' as const,
function normalizeReportOverview(data: ReportOverviewResponseDto): ReportOverviewResponseDto {
return {
timezone: data.timezone ?? 'Asia/Shanghai',
generatedAt: data.generatedAt ?? '',
snapshotAt: data.snapshotAt ?? '',
metrics: {
initiatedCount: data.metrics?.initiatedCount ?? 0,
signedCount: data.metrics?.signedCount ?? 0,
onTimeSignedCount: data.metrics?.onTimeSignedCount ?? 0,
onTimeSampleCount: data.metrics?.onTimeSampleCount ?? 0,
onTimeRate: data.metrics?.onTimeRate ?? null,
averageSignDurationSeconds: data.metrics?.averageSignDurationSeconds ?? null,
durationSampleCount: data.metrics?.durationSampleCount ?? 0,
},
trend: data.trend ?? [],
departments: data.departments ?? [],
}
const firstPage = await getSigningTasks(query, options)
const records = [...firstPage.records]
const pageSize = Math.max(firstPage.pageSize, 1)
const totalPages = Math.ceil(firstPage.total / pageSize)
for (let page = 2; page <= totalPages; page += 1) {
const nextPage = await getSigningTasks({ ...query, page }, options)
records.push(...nextPage.records)
if (!nextPage.records.length) {
break
}
}
return records
}
/**
* 查询报表所需的全部可见任务。
* 当前后端没有聚合报表接口,因此这里按最大页大小读取签署任务;聚合接口上线后只需替换本函数。
* ReportFilter/ReportTasksQuery are object query parameters in OpenAPI's default
* form/explode representation, so fields are sent flat (createdFrom=..., etc.).
*/
export async function getReportTaskData(): Promise<ReportTaskData> {
if (isReportsMockEnabled) {
return { tasks: [], templates: [], campuses: [], departments: [] }
}
export async function getReportOptions(
filter: ReportFilterDto = {},
): Promise<ReportOptionsResponseDto> {
const response = await request.get<ApiResponse<ReportOptionsResponseDto>>(
'/v1/reports/signing/options',
{ params: compactQuery(filter) },
)
const [templatesResult, campusesResult, departmentsResult] = await Promise.allSettled([
getSigningTemplates(),
getCampuses(),
getDepartments(),
])
const data: ReportTaskData = {
tasks: [],
templates: templatesResult.status === 'fulfilled' ? templatesResult.value : [],
campuses: campusesResult.status === 'fulfilled' ? campusesResult.value : [],
departments: departmentsResult.status === 'fulfilled' ? departmentsResult.value : [],
}
data.tasks = await getAllReportTasks(data)
return data
return normalizeReportOptions(unwrapApiResponse(response))
}
export async function getReportOverview(
filter: ReportFilterDto,
): Promise<ReportOverviewResponseDto> {
const response = await request.get<ApiResponse<ReportOverviewResponseDto>>(
'/v1/reports/signing/overview',
{ params: compactQuery(filter) },
)
return normalizeReportOverview(unwrapApiResponse(response))
}
export async function getReportTasks(
query: ReportTasksQueryDto,
): Promise<ReportPageResponseDto<ReportTaskResponseDto>> {
const response = await request.get<ApiResponse<ReportPageResponseDto<ReportTaskResponseDto>>>(
'/v1/reports/signing/tasks',
{ params: compactQuery(query) },
)
const page = unwrapApiResponse(response)
return {
records: page.records ?? [],
page: page.page,
size: page.size,
total: page.total,
pages: page.pages,
}
}
export async function createReportExportJob(
payload: ReportExportRequestDto,
): Promise<ReportExportJobResponseDto> {
const response = await request.post<ApiResponse<ReportExportJobResponseDto>>(
'/v1/reports/signing/export-jobs',
payload,
)
return unwrapApiResponse(response)
}
export async function getReportExportJob(jobId: string): Promise<ReportExportJobResponseDto> {
const response = await request.get<ApiResponse<ReportExportJobResponseDto>>(
`/v1/reports/signing/export-jobs/${encodeURIComponent(jobId)}`,
)
return unwrapApiResponse(response)
}
export async function downloadReportExportFile(jobId: string): Promise<Blob> {
return request.get<Blob>(
`/v1/reports/signing/export-jobs/${encodeURIComponent(jobId)}/download`,
{ responseType: 'blob' },
)
}
+129 -10
View File
@@ -1,5 +1,4 @@
import type { PageQuery, PageResult } from '@/types/common'
import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types'
export type DocumentStatus = 'draft' | 'published' | 'archived' | 'review'
@@ -261,15 +260,135 @@ export interface OrganizationResponseDto {
departmentName?: string | null
}
/**
* 报表当前由签署任务列表、模板和组织字典在前端聚合。
* 后端提供聚合报表接口后,可在 reports.ts 的边界替换为对应 DTO,页面无需感知。
*/
export interface ReportTaskData {
tasks: SigningTaskRecord[]
templates: SigningTemplate[]
campuses: CampusRecord[]
departments: DepartmentRecord[]
export type ReportBackendTaskStatus =
'CREATED' | 'WAITING_SIGN' | 'SIGNED' | 'EXPIRED' | 'VOIDED' | 'GENERATING' | 'FAILED'
export type ReportBackendSignMethod = 'PAD' | 'SMS'
export type ReportTimeDimension = 'CREATED' | 'SIGNED' | 'EXPIRED'
export type ReportSortDirection = 'asc' | 'desc'
export type ReportExportFormat = 'CSV' | 'XLSX'
/** /api/v1/reports/signing 的公共过滤 DTO;时间区间为左闭右开。 */
export interface ReportFilterDto {
createdFrom?: string
createdTo?: string
signedFrom?: string
signedTo?: string
expiredFrom?: string
expiredTo?: string
campusId?: string
departmentId?: string
templateVersionId?: string
templateCategory?: string
signMethod?: ReportBackendSignMethod
keyword?: string
timezone?: string
}
export interface ReportTasksQueryDto extends ReportFilterDto {
page?: number
size?: number
timeDimension?: ReportTimeDimension
sort?: string
direction?: ReportSortDirection
}
export interface ReportExportRequestDto extends ReportTasksQueryDto {
format: ReportExportFormat
columns?: string[]
}
export interface ReportMetricsDto {
initiatedCount: number
signedCount: number
onTimeSignedCount: number
onTimeSampleCount: number
onTimeRate: number | null
averageSignDurationSeconds: number | null
durationSampleCount: number
}
export interface ReportTrendPointDto {
date: string
label: string
signedCount: number
}
export interface ReportDepartmentBreakdownDto {
departmentId?: string | null
departmentName?: string | null
initiatedCount?: number | null
signedCount?: number | null
onTimeSignedCount?: number | null
onTimeSampleCount?: number | null
onTimeRate?: number | null
}
export interface ReportOverviewResponseDto {
timezone: string
generatedAt: string
snapshotAt: string
metrics: ReportMetricsDto
trend: ReportTrendPointDto[]
departments: ReportDepartmentBreakdownDto[]
}
export interface ReportTaskResponseDto {
taskId: string
patientDisplayId: string
patientNameMasked: string
patientMobileMasked?: string | null
status: ReportBackendTaskStatus
signMethod: ReportBackendSignMethod
createdAt: string
deadlineAt?: string | null
signStartedAt?: string | null
signedAt?: string | null
expiredAt?: string | null
voidedAt?: string | null
signDurationSeconds?: number | null
onTime?: boolean | null
templateVersionId: string
templateVersionNumber?: number | null
templateName: string
templateCategory: string
campusId: string
campusName: string
departmentId?: string | null
departmentName: string
}
export interface ReportOptionsResponseDto {
campuses: Array<{ id: string; name: string }>
departments: Array<{ id: string; campusId: string; name: string }>
templateCategories: string[]
}
export interface ReportPageResponseDto<T> {
records: T[]
page: number
size: number
total: number
pages: number
}
export type ReportExportJobStatus = 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED'
export interface ReportExportJobResponseDto {
jobId: string
status: ReportExportJobStatus
format?: ReportExportFormat | string | null
requestedAt?: string | null
startedAt?: string | null
completedAt?: string | null
expiresAt?: string | null
resultCount?: number | null
errorCode?: string | null
errorMessage?: string | null
downloadPath?: string | null
}
export type UserStatus = 'enabled' | 'disabled'