198 lines
5.6 KiB
TypeScript
198 lines
5.6 KiB
TypeScript
import axios from 'axios'
|
|
|
|
import { unwrapApiResponse } from '@/utils/api-response'
|
|
import { mockFlags } from '@/utils/mock-flags'
|
|
import { request } from '@/utils/request'
|
|
|
|
import { getMockSigningTask } from './signing'
|
|
import type {
|
|
ApiPageResponse,
|
|
ApiResponseOf,
|
|
SignArtifactResponseDto,
|
|
SignArtifactType,
|
|
SigningArtifact,
|
|
} from './types'
|
|
|
|
const useMockData = mockFlags.signing
|
|
|
|
type ArtifactCollection = SignArtifactResponseDto[] | ApiPageResponse<SignArtifactResponseDto>
|
|
|
|
const artifactLabels: Record<string, string> = {
|
|
ORIGINAL_PDF: 'PDF 原件',
|
|
SIGNATURE_IMAGE: '签名原图',
|
|
SIGNED_PDF: '签署后 PDF',
|
|
}
|
|
|
|
function formatDateTime(value: string | null | undefined) {
|
|
if (!value) {
|
|
return ''
|
|
}
|
|
|
|
const date = new Date(value)
|
|
|
|
if (Number.isNaN(date.getTime())) {
|
|
return value
|
|
.replace('T', ' ')
|
|
.replace(/\.\d+Z$/, '')
|
|
.replace(/Z$/, '')
|
|
}
|
|
|
|
const pad = (part: number) => String(part).padStart(2, '0')
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
|
|
date.getHours(),
|
|
)}:${pad(date.getMinutes())}`
|
|
}
|
|
|
|
function getCollectionRecords(collection: ArtifactCollection) {
|
|
return Array.isArray(collection) ? collection : collection.records
|
|
}
|
|
|
|
function normalizeArtifact(dto: SignArtifactResponseDto): SigningArtifact {
|
|
const artifactType: SignArtifactType = dto.artifactType
|
|
const label = artifactLabels[artifactType] ?? '签署文件'
|
|
const mimeType = dto.mimeType || 'application/octet-stream'
|
|
const extension =
|
|
mimeType === 'application/pdf' ? 'pdf' : mimeType === 'image/png' ? 'png' : 'bin'
|
|
|
|
return {
|
|
id: dto.id,
|
|
taskId: dto.taskId,
|
|
pipelineId: dto.pipelineId,
|
|
artifactType,
|
|
label,
|
|
fileName: `${label}.${extension}`,
|
|
mimeType,
|
|
sizeBytes: dto.sizeBytes,
|
|
sha256: dto.sha256,
|
|
metadata: dto.metadata ?? {},
|
|
createdAt: formatDateTime(dto.createdAt),
|
|
}
|
|
}
|
|
|
|
export const isArtifactMockEnabled = useMockData
|
|
|
|
/**
|
|
* 演示签名原图产物的 id 前缀。
|
|
* 演示环境没有文件服务,下载时靠 id 反查任务里的签名图,
|
|
* 保证「签名原图」在演示里也是真能下载到的文件,而不是占位提示。
|
|
*/
|
|
const MOCK_SIGNATURE_PREFIX = 'mock-signature-'
|
|
|
|
function estimateDataUrlBytes(dataUrl: string) {
|
|
const payload = dataUrl.split(',')[1]
|
|
|
|
if (!payload) {
|
|
return 0
|
|
}
|
|
|
|
// base64 每 4 个字符还原 3 字节,末尾可能有填充,这里取近似值用于展示
|
|
return Math.floor((payload.replace(/=+$/, '').length * 3) / 4)
|
|
}
|
|
|
|
/** 演示产物:已签署且有签名图时,给出可下载的签名原图 */
|
|
function buildMockArtifacts(taskId: string): SigningArtifact[] {
|
|
const task = getMockSigningTask(taskId)
|
|
|
|
if (!task || task.status !== 'signed' || !task.signatureDataUrl) {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
{
|
|
id: `${MOCK_SIGNATURE_PREFIX}${task.id}`,
|
|
taskId: task.id,
|
|
pipelineId: `mock-pipeline-${task.id}`,
|
|
artifactType: 'SIGNATURE_IMAGE',
|
|
label: artifactLabels.SIGNATURE_IMAGE ?? '签名原图',
|
|
fileName: '签名原图.png',
|
|
mimeType: 'image/png',
|
|
sizeBytes: estimateDataUrlBytes(task.signatureDataUrl),
|
|
sha256: '',
|
|
metadata: {},
|
|
createdAt: formatDateTime(task.signedAt ?? task.updatedAt),
|
|
},
|
|
]
|
|
}
|
|
|
|
/** 把签名图 dataURL 还原成可下载的二进制 Blob */
|
|
function dataUrlToBlob(dataUrl: string): Blob {
|
|
const [meta, payload] = dataUrl.split(',')
|
|
const mimeType = /data:([^;]+)/.exec(meta ?? '')?.[1] ?? 'application/octet-stream'
|
|
|
|
if (!payload) {
|
|
return new Blob([], { type: mimeType })
|
|
}
|
|
|
|
const binary = window.atob(payload)
|
|
const bytes = new Uint8Array(binary.length)
|
|
|
|
for (let index = 0; index < binary.length; index += 1) {
|
|
bytes[index] = binary.charCodeAt(index)
|
|
}
|
|
|
|
return new Blob([bytes], { type: mimeType })
|
|
}
|
|
|
|
export async function getSigningArtifacts(taskId: string): Promise<SigningArtifact[]> {
|
|
if (useMockData) {
|
|
return buildMockArtifacts(taskId)
|
|
}
|
|
|
|
try {
|
|
const response = await request.get<ApiResponseOf<ArtifactCollection>>(
|
|
`/v1/sign-artifacts/task/${encodeURIComponent(taskId)}`,
|
|
)
|
|
|
|
return getCollectionRecords(unwrapApiResponse(response)).map(normalizeArtifact)
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
|
return []
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export async function getSigningArtifact(
|
|
taskId: string,
|
|
artifactId: string,
|
|
): Promise<SigningArtifact | null> {
|
|
if (useMockData) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const response = await request.get<ApiResponseOf<SignArtifactResponseDto>>(
|
|
`/v1/sign-artifacts/task/${encodeURIComponent(taskId)}/${encodeURIComponent(artifactId)}`,
|
|
)
|
|
|
|
return normalizeArtifact(unwrapApiResponse(response))
|
|
} catch (error) {
|
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
|
return null
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export function downloadSigningArtifact(artifactId: string): Promise<Blob> {
|
|
if (useMockData) {
|
|
// 演示环境没有文件服务,按 id 反查任务里的签名图,返回真实的 PNG
|
|
if (artifactId.startsWith(MOCK_SIGNATURE_PREFIX)) {
|
|
const taskId = artifactId.slice(MOCK_SIGNATURE_PREFIX.length)
|
|
const signatureDataUrl = getMockSigningTask(taskId)?.signatureDataUrl
|
|
|
|
if (signatureDataUrl) {
|
|
return Promise.resolve(dataUrlToBlob(signatureDataUrl))
|
|
}
|
|
}
|
|
|
|
return Promise.resolve(new Blob(['Mock 签署文件,仅用于界面演示。'], { type: 'text/plain' }))
|
|
}
|
|
|
|
return request.get<Blob>(`/v1/sign-artifacts/${encodeURIComponent(artifactId)}/download`, {
|
|
responseType: 'blob',
|
|
})
|
|
}
|