feat(api): 接入医签通业务接口与类型
This commit is contained in:
82
clinical-web/src/api/management/audit.ts
Normal file
82
clinical-web/src/api/management/audit.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { unwrapApiResponse } from '@/utils/api-response'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
import type { ApiResponse, PageResult } from '@/types/common'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AuditLogListResponse,
|
||||||
|
AuditLogQuery,
|
||||||
|
AuditLogRecord,
|
||||||
|
AuditLogResponseDto,
|
||||||
|
BackendCollection,
|
||||||
|
BackendPage,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||||
|
|
||||||
|
function formatDateTime(value: string) {
|
||||||
|
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 mapAuditLog(dto: AuditLogResponseDto): AuditLogRecord {
|
||||||
|
return {
|
||||||
|
id: dto.id,
|
||||||
|
action: dto.action,
|
||||||
|
resourceType: dto.resourceType,
|
||||||
|
resourceId: dto.resourceId,
|
||||||
|
operatorId: dto.operatorId,
|
||||||
|
clientIp: dto.clientIp ?? '—',
|
||||||
|
userAgent: dto.userAgent ?? '—',
|
||||||
|
detailsJson: dto.detailsJson ?? '',
|
||||||
|
createdAt: formatDateTime(dto.createdAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePage<T>(
|
||||||
|
data: BackendPage<T> | T[],
|
||||||
|
fallbackPage: number,
|
||||||
|
fallbackPageSize: number,
|
||||||
|
): PageResult<T> {
|
||||||
|
const records = Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
records,
|
||||||
|
total: Array.isArray(data) ? records.length : (data.total ?? records.length),
|
||||||
|
page: Array.isArray(data) ? fallbackPage : (data.page ?? fallbackPage),
|
||||||
|
pageSize: Array.isArray(data) ? fallbackPageSize : (data.size ?? fallbackPageSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuditLogs(query: AuditLogQuery): Promise<AuditLogListResponse> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.resolve({ records: [], total: 0, page: query.page, pageSize: query.pageSize })
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: Record<string, string | number> = {
|
||||||
|
page: Math.max(query.page, 1),
|
||||||
|
size: Math.min(Math.max(query.pageSize, 1), 200),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.keyword?.trim()) {
|
||||||
|
params.keyword = query.keyword.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get<ApiResponse<BackendCollection<AuditLogResponseDto>>>('/v1/audit-logs', { params })
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then((data) => {
|
||||||
|
const page = normalizePage(data, query.page, query.pageSize)
|
||||||
|
return { ...page, records: page.records.map(mapAuditLog) }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
|
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
|
import type { ApiResponse } from '@/types/common'
|
||||||
|
|
||||||
import type { PermissionListResponse, PermissionQuery, PermissionRecord } from './types'
|
import type {
|
||||||
|
BackendCollection,
|
||||||
|
CreateTemplatePermissionRequest,
|
||||||
|
PermissionListResponse,
|
||||||
|
PermissionQuery,
|
||||||
|
PermissionRecord,
|
||||||
|
TemplatePermissionRecord,
|
||||||
|
TemplatePermissionResponseDto,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||||
|
|
||||||
|
export const isPermissionMockEnabled = useMockData
|
||||||
|
|
||||||
const mockRecords: PermissionRecord[] = [
|
const mockRecords: PermissionRecord[] = [
|
||||||
{
|
{
|
||||||
id: 'permission-001',
|
id: 'permission-001',
|
||||||
@@ -25,9 +37,9 @@ const mockRecords: PermissionRecord[] = [
|
|||||||
|
|
||||||
export function getDocumentPermissions(query: PermissionQuery): Promise<PermissionListResponse> {
|
export function getDocumentPermissions(query: PermissionQuery): Promise<PermissionListResponse> {
|
||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
return request.get<PermissionListResponse>('/management/document-permissions', {
|
return Promise.reject(
|
||||||
params: query,
|
new Error('MEDISIGN 未提供跨模板权限汇总接口,请使用 getTemplatePermissions'),
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyword = query.keyword?.trim().toLowerCase()
|
const keyword = query.keyword?.trim().toLowerCase()
|
||||||
@@ -51,3 +63,80 @@ export function getDocumentPermissions(query: PermissionQuery): Promise<Permissi
|
|||||||
pageSize,
|
pageSize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 mapTemplatePermission(dto: TemplatePermissionResponseDto): TemplatePermissionRecord {
|
||||||
|
return {
|
||||||
|
id: dto.id,
|
||||||
|
templateId: dto.templateId,
|
||||||
|
subjectType: dto.subjectType,
|
||||||
|
subjectId: dto.subjectId,
|
||||||
|
subjectName: dto.subjectId,
|
||||||
|
permissionLevel: dto.permissionLevel,
|
||||||
|
effect: dto.effect,
|
||||||
|
inherited: dto.inherited,
|
||||||
|
createdAt: formatDateTime(dto.createdAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTemplatePermissions(
|
||||||
|
templateId: string,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<TemplatePermissionRecord[]> {
|
||||||
|
if (useMockData) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.get<ApiResponse<BackendCollection<TemplatePermissionResponseDto>>>(
|
||||||
|
`/v1/templates/${encodeURIComponent(templateId)}/permissions`,
|
||||||
|
{ params: userId ? { userId } : undefined },
|
||||||
|
)
|
||||||
|
const data = unwrapApiResponse(response)
|
||||||
|
const records = Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? [])
|
||||||
|
return records.map(mapTemplatePermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTemplatePermission(
|
||||||
|
templateId: string,
|
||||||
|
payload: CreateTemplatePermissionRequest,
|
||||||
|
): Promise<TemplatePermissionRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行权限写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.post<ApiResponse<TemplatePermissionResponseDto>>(
|
||||||
|
`/v1/templates/${encodeURIComponent(templateId)}/permissions`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return mapTemplatePermission(unwrapApiResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteTemplatePermission(templateId: string, permissionId: string) {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行权限写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.delete<ApiResponse<null>>(
|
||||||
|
`/v1/templates/${encodeURIComponent(templateId)}/permissions/${encodeURIComponent(permissionId)}`,
|
||||||
|
)
|
||||||
|
unwrapNullableApiResponse(response)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import { request } from '@/utils/request'
|
import axios from 'axios'
|
||||||
|
|
||||||
import type { DocumentListResponse, DocumentQuery, DocumentRecord } from './types'
|
import { unwrapApiResponse } from '@/utils/api-response'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
import type { ApiResponse, PageResult } from '@/types/common'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
BackendCollection,
|
||||||
|
BackendPage,
|
||||||
|
BackendTemplateStatus,
|
||||||
|
CreateTemplateRequest,
|
||||||
|
CreateTemplateVersionRequest,
|
||||||
|
DocumentListResponse,
|
||||||
|
DocumentQuery,
|
||||||
|
DocumentRecord,
|
||||||
|
TemplateResponseDto,
|
||||||
|
TemplateVersionResponseDto,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||||
|
|
||||||
|
export const isDocumentMockEnabled = useMockData
|
||||||
|
|
||||||
const mockRecords: DocumentRecord[] = [
|
const mockRecords: DocumentRecord[] = [
|
||||||
{
|
{
|
||||||
id: 'doc-001',
|
id: 'doc-001',
|
||||||
@@ -25,10 +42,118 @@ const mockRecords: DocumentRecord[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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 normalizeDocumentStatus(status: BackendTemplateStatus | null | undefined) {
|
||||||
|
switch (status) {
|
||||||
|
case 'PUBLISHED':
|
||||||
|
case 'APPROVED':
|
||||||
|
return 'published' as const
|
||||||
|
case 'ARCHIVED':
|
||||||
|
case 'DISABLED':
|
||||||
|
return 'archived' as const
|
||||||
|
case 'PENDING_REVIEW':
|
||||||
|
case 'REJECTED':
|
||||||
|
return 'review' as const
|
||||||
|
case 'DRAFT':
|
||||||
|
default:
|
||||||
|
return 'draft' as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDocument(dto: TemplateResponseDto): DocumentRecord {
|
||||||
|
const versionId = dto.currentVersionId ?? undefined
|
||||||
|
const version = dto.versionSequence ? `v${dto.versionSequence}` : '—'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.name,
|
||||||
|
code: dto.templateCode,
|
||||||
|
category: dto.category ?? '未分类',
|
||||||
|
version: String(version),
|
||||||
|
status: normalizeDocumentStatus(dto.status),
|
||||||
|
backendStatus: dto.status ?? undefined,
|
||||||
|
updatedBy: dto.updatedBy ?? dto.createdBy ?? '—',
|
||||||
|
updatedAt: formatDateTime(dto.updatedAt ?? dto.createdAt),
|
||||||
|
versionId,
|
||||||
|
departmentId: dto.departmentId,
|
||||||
|
departmentName: dto.departmentId ? undefined : '全院通用',
|
||||||
|
campusId: dto.campusId,
|
||||||
|
description: dto.description ?? '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePage<T>(
|
||||||
|
data: BackendPage<T> | T[],
|
||||||
|
fallbackPage: number,
|
||||||
|
fallbackPageSize: number,
|
||||||
|
): PageResult<T> {
|
||||||
|
const records = Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? [])
|
||||||
|
const page = Array.isArray(data) ? fallbackPage : (data.page ?? fallbackPage)
|
||||||
|
const pageSize = Array.isArray(data) ? fallbackPageSize : (data.size ?? fallbackPageSize)
|
||||||
|
|
||||||
|
return {
|
||||||
|
records,
|
||||||
|
total: Array.isArray(data) ? records.length : (data.total ?? records.length),
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBackendQuery(query: DocumentQuery) {
|
||||||
|
const params: Record<string, string | number> = {
|
||||||
|
page: Math.max(query.page, 1),
|
||||||
|
size: Math.min(Math.max(query.pageSize, 1), 200),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.keyword?.trim()) {
|
||||||
|
params.keyword = query.keyword.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.status && query.status !== 'all') {
|
||||||
|
const statusMap: Record<Exclude<DocumentQuery['status'], undefined | 'all'>, string> = {
|
||||||
|
draft: 'DRAFT',
|
||||||
|
published: 'PUBLISHED',
|
||||||
|
archived: 'ARCHIVED',
|
||||||
|
review: 'PENDING_REVIEW',
|
||||||
|
}
|
||||||
|
params.status = statusMap[query.status]
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
export function getDocuments(query: DocumentQuery): Promise<DocumentListResponse> {
|
export function getDocuments(query: DocumentQuery): Promise<DocumentListResponse> {
|
||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
return request.get<DocumentListResponse>('/management/documents', {
|
return request
|
||||||
params: query,
|
.get<ApiResponse<BackendCollection<TemplateResponseDto>>>('/v1/templates', {
|
||||||
|
params: toBackendQuery(query),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const page = normalizePage(unwrapApiResponse(response), query.page, query.pageSize)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...page,
|
||||||
|
records: page.records.map(mapDocument),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,17 +169,111 @@ export function getDocuments(query: DocumentQuery): Promise<DocumentListResponse
|
|||||||
const start = (page - 1) * pageSize
|
const start = (page - 1) * pageSize
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
records: filteredRecords.slice(start, start + pageSize),
|
records: filteredRecords.slice(start, start + pageSize).map((record) => ({ ...record })),
|
||||||
total: filteredRecords.length,
|
total: filteredRecords.length,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDocumentDetail(id: string): Promise<DocumentRecord | null> {
|
export async function getDocumentDetail(id: string): Promise<DocumentRecord | null> {
|
||||||
if (!useMockData) {
|
if (useMockData) {
|
||||||
return request.get<DocumentRecord>('/management/documents/' + id)
|
const record = mockRecords.find((item) => item.id === id)
|
||||||
|
return record ? { ...record } : null
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(mockRecords.find((record) => record.id === id) ?? null)
|
try {
|
||||||
|
const response = await request.get<ApiResponse<TemplateResponseDto>>(
|
||||||
|
`/v1/templates/${encodeURIComponent(id)}`,
|
||||||
|
)
|
||||||
|
return mapDocument(unwrapApiResponse(response))
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTemplate(payload: CreateTemplateRequest): Promise<DocumentRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行模板写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.post<ApiResponse<TemplateResponseDto>>('/v1/templates', payload)
|
||||||
|
return mapDocument(unwrapApiResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTemplateVersion(
|
||||||
|
templateId: string,
|
||||||
|
payload: CreateTemplateVersionRequest,
|
||||||
|
): Promise<TemplateVersionResponseDto> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行模板写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.post<ApiResponse<TemplateVersionResponseDto>>(
|
||||||
|
`/v1/templates/${encodeURIComponent(templateId)}/versions`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return unwrapApiResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTemplateVersions(
|
||||||
|
templateId: string,
|
||||||
|
): Promise<TemplateVersionResponseDto[]> {
|
||||||
|
if (useMockData) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.get<ApiResponse<BackendCollection<TemplateVersionResponseDto>>>(
|
||||||
|
`/v1/templates/${encodeURIComponent(templateId)}/versions`,
|
||||||
|
)
|
||||||
|
return normalizePage(unwrapApiResponse(response), 1, 200).records
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTemplateVersionDetail(
|
||||||
|
templateId: string,
|
||||||
|
versionId: string,
|
||||||
|
): Promise<TemplateVersionResponseDto | null> {
|
||||||
|
if (useMockData) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request.get<ApiResponse<TemplateVersionResponseDto>>(
|
||||||
|
`/v1/templates/${encodeURIComponent(templateId)}/versions/${encodeURIComponent(versionId)}`,
|
||||||
|
)
|
||||||
|
return unwrapApiResponse(response)
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TemplateVersionAction =
|
||||||
|
'submit-review' | 'reject' | 'approve' | 'publish' | 'disable' | 'archive'
|
||||||
|
|
||||||
|
export function updateTemplateVersionStatus(
|
||||||
|
templateId: string,
|
||||||
|
versionId: string,
|
||||||
|
action: TemplateVersionAction,
|
||||||
|
reason?: string,
|
||||||
|
): Promise<TemplateVersionResponseDto> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行模板版本工作流'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestData = action === 'reject' && reason?.trim() ? { comment: reason.trim() } : undefined
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post<ApiResponse<TemplateVersionResponseDto>>(
|
||||||
|
`/v1/templates/${encodeURIComponent(templateId)}/versions/${encodeURIComponent(versionId)}/${action}`,
|
||||||
|
requestData,
|
||||||
|
)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
}
|
}
|
||||||
|
|||||||
159
clinical-web/src/api/management/organization.ts
Normal file
159
clinical-web/src/api/management/organization.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
import type { ApiResponse } from '@/types/common'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
BackendCollection,
|
||||||
|
CampusRecord,
|
||||||
|
CreateCampusRequest,
|
||||||
|
CreateDepartmentRequest,
|
||||||
|
DepartmentRecord,
|
||||||
|
OrganizationResponseDto,
|
||||||
|
UpdateCampusRequest,
|
||||||
|
UpdateDepartmentRequest,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||||
|
|
||||||
|
function getRecords(data: BackendCollection<OrganizationResponseDto>) {
|
||||||
|
return Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCampuses(): Promise<CampusRecord[]> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.resolve([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get<ApiResponse<BackendCollection<OrganizationResponseDto>>>('/v1/campuses')
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then((data) =>
|
||||||
|
getRecords(data).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
code: item.code ?? item.id,
|
||||||
|
name: item.name ?? item.campusName ?? item.id,
|
||||||
|
address: item.address ?? undefined,
|
||||||
|
status: item.status ?? 'ENABLED',
|
||||||
|
sortNo: item.sortNo ?? 0,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDepartments(campusId?: string): Promise<DepartmentRecord[]> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.resolve([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get<ApiResponse<BackendCollection<OrganizationResponseDto>>>('/v1/departments', {
|
||||||
|
params: campusId ? { campusId } : undefined,
|
||||||
|
})
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then((data) =>
|
||||||
|
getRecords(data).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
campusId: item.campusId ?? '',
|
||||||
|
parentId: item.parentId,
|
||||||
|
code: item.code ?? item.id,
|
||||||
|
name: item.name ?? item.departmentName ?? item.id,
|
||||||
|
status: item.status ?? 'ENABLED',
|
||||||
|
sortNo: item.sortNo ?? 0,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapCampus(data: OrganizationResponseDto): CampusRecord {
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
code: data.code ?? data.id,
|
||||||
|
name: data.name ?? data.campusName ?? data.id,
|
||||||
|
address: data.address ?? undefined,
|
||||||
|
status: data.status ?? 'ENABLED',
|
||||||
|
sortNo: data.sortNo ?? 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDepartment(data: OrganizationResponseDto): DepartmentRecord {
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
campusId: data.campusId ?? '',
|
||||||
|
parentId: data.parentId,
|
||||||
|
code: data.code ?? data.id,
|
||||||
|
name: data.name ?? data.departmentName ?? data.id,
|
||||||
|
status: data.status ?? 'ENABLED',
|
||||||
|
sortNo: data.sortNo ?? 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCampus(payload: CreateCampusRequest): Promise<CampusRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行院区写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.post<ApiResponse<OrganizationResponseDto>>('/v1/campuses', payload)
|
||||||
|
return mapCampus(unwrapApiResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCampus(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateCampusRequest,
|
||||||
|
): Promise<CampusRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行院区写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.put<ApiResponse<OrganizationResponseDto>>(
|
||||||
|
`/v1/campuses/${encodeURIComponent(id)}`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return mapCampus(unwrapApiResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCampus(id: string): Promise<void> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行院区写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.delete<ApiResponse<null>>(`/v1/campuses/${encodeURIComponent(id)}`)
|
||||||
|
unwrapNullableApiResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createDepartment(
|
||||||
|
payload: CreateDepartmentRequest,
|
||||||
|
): Promise<DepartmentRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行科室写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.post<ApiResponse<OrganizationResponseDto>>(
|
||||||
|
'/v1/departments',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return mapDepartment(unwrapApiResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateDepartment(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateDepartmentRequest,
|
||||||
|
): Promise<DepartmentRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行科室写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.put<ApiResponse<OrganizationResponseDto>>(
|
||||||
|
`/v1/departments/${encodeURIComponent(id)}`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
return mapDepartment(unwrapApiResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDepartment(id: string): Promise<void> {
|
||||||
|
if (useMockData) {
|
||||||
|
throw new Error('Mock 模式不执行科室写操作')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request.delete<ApiResponse<null>>(
|
||||||
|
`/v1/departments/${encodeURIComponent(id)}`,
|
||||||
|
)
|
||||||
|
unwrapNullableApiResponse(response)
|
||||||
|
}
|
||||||
79
clinical-web/src/api/management/roles.ts
Normal file
79
clinical-web/src/api/management/roles.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { unwrapApiResponse } from '@/utils/api-response'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
import type { ApiResponse } from '@/types/common'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
BackendCollection,
|
||||||
|
CreateRoleRequest,
|
||||||
|
RoleRecord,
|
||||||
|
RoleResponseDto,
|
||||||
|
UpdateRoleRequest,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||||
|
|
||||||
|
const fallbackColors = ['#6b5bd2', '#0e6e8c', '#0f9d6c', '#d9821f', '#c65d5d']
|
||||||
|
|
||||||
|
function normalizeRole(dto: RoleResponseDto, index: number): RoleRecord {
|
||||||
|
return {
|
||||||
|
id: dto.id,
|
||||||
|
code: dto.code,
|
||||||
|
name: dto.name,
|
||||||
|
description: dto.description ?? '—',
|
||||||
|
color: fallbackColors[index % fallbackColors.length],
|
||||||
|
userCount: dto.userCount ?? 0,
|
||||||
|
status: dto.status ?? 'ENABLED',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRoles(): Promise<RoleRecord[]> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.resolve([])
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.get<ApiResponse<BackendCollection<RoleResponseDto>>>('/v1/roles')
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then((data) => {
|
||||||
|
const records = Array.isArray(data)
|
||||||
|
? data
|
||||||
|
: (data.records ?? data.items ?? data.content ?? [])
|
||||||
|
return records.map(normalizeRole)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRole(payload: CreateRoleRequest): Promise<RoleRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行角色写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post<ApiResponse<RoleResponseDto>>('/v1/roles', payload)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then((role) => normalizeRole(role, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateRole(id: string, payload: UpdateRoleRequest): Promise<RoleRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行角色写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.put<ApiResponse<RoleResponseDto>>(`/v1/roles/${encodeURIComponent(id)}`, payload)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then((role) => normalizeRole(role, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteRole(id: string): Promise<void> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行角色写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.delete<ApiResponse<null>>(`/v1/roles/${encodeURIComponent(id)}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.code !== 0 && response.code !== '0') {
|
||||||
|
throw new Error(response.message || '角色删除失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PageQuery, PageResult } from '@/types/common'
|
import type { PageQuery, PageResult } from '@/types/common'
|
||||||
|
|
||||||
export type DocumentStatus = 'draft' | 'published' | 'archived'
|
export type DocumentStatus = 'draft' | 'published' | 'archived' | 'review'
|
||||||
|
|
||||||
export interface DocumentQuery extends PageQuery {
|
export interface DocumentQuery extends PageQuery {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
@@ -10,15 +10,198 @@ export interface DocumentQuery extends PageQuery {
|
|||||||
export interface DocumentRecord {
|
export interface DocumentRecord {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
code?: string
|
||||||
category: string
|
category: string
|
||||||
version: string
|
version: string
|
||||||
status: DocumentStatus
|
status: DocumentStatus
|
||||||
|
backendStatus?: BackendTemplateStatus
|
||||||
updatedBy: string
|
updatedBy: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
versionId?: string
|
||||||
|
departmentId?: string | null
|
||||||
|
departmentName?: string
|
||||||
|
campusId?: string | null
|
||||||
|
campusName?: string
|
||||||
|
description?: string
|
||||||
|
contentHtml?: string
|
||||||
|
contentSha256?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentListResponse = PageResult<DocumentRecord>
|
export type DocumentListResponse = PageResult<DocumentRecord>
|
||||||
|
|
||||||
|
export type BackendTemplateStatus =
|
||||||
|
| 'DRAFT'
|
||||||
|
| 'PENDING_REVIEW'
|
||||||
|
| 'REJECTED'
|
||||||
|
| 'APPROVED'
|
||||||
|
| 'PUBLISHED'
|
||||||
|
| 'DISABLED'
|
||||||
|
| 'ARCHIVED'
|
||||||
|
| string
|
||||||
|
|
||||||
|
export interface TemplateResponseDto {
|
||||||
|
id: string
|
||||||
|
templateCode: string
|
||||||
|
name: string
|
||||||
|
description?: string | null
|
||||||
|
campusId: string
|
||||||
|
departmentId?: string | null
|
||||||
|
category?: string | null
|
||||||
|
status?: BackendTemplateStatus | null
|
||||||
|
currentVersionId?: string | null
|
||||||
|
versionSequence?: number | null
|
||||||
|
createdAt?: string | null
|
||||||
|
createdBy?: string | null
|
||||||
|
updatedAt?: string | null
|
||||||
|
updatedBy?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateVersionResponseDto {
|
||||||
|
id: string
|
||||||
|
templateId: string
|
||||||
|
versionNo?: string | null
|
||||||
|
versionNumber?: number | null
|
||||||
|
contentHtml?: string | null
|
||||||
|
content?: string | null
|
||||||
|
signatureFields?: unknown[] | null
|
||||||
|
contentSha256?: string | null
|
||||||
|
requestFingerprint?: string | null
|
||||||
|
fileSizeBytes?: number | null
|
||||||
|
fileMimeType?: string | null
|
||||||
|
fileSha256?: string | null
|
||||||
|
fileMetadata?: Record<string, unknown> | null
|
||||||
|
status?: BackendTemplateStatus | null
|
||||||
|
effectiveAt?: string | null
|
||||||
|
disabledAt?: string | null
|
||||||
|
reviewSubmittedAt?: string | null
|
||||||
|
reviewedAt?: string | null
|
||||||
|
reviewedBy?: string | null
|
||||||
|
reviewComment?: string | null
|
||||||
|
publishedAt?: string | null
|
||||||
|
createdAt?: string | null
|
||||||
|
createdBy?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTemplateRequest {
|
||||||
|
templateCode: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
campusId: string
|
||||||
|
departmentId?: string | null
|
||||||
|
category?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTemplateVersionRequest {
|
||||||
|
versionNo?: string
|
||||||
|
contentHtml: string
|
||||||
|
signatureFields?: unknown[]
|
||||||
|
fileStorageKey?: string
|
||||||
|
fileRelativePath?: string
|
||||||
|
fileSizeBytes?: number
|
||||||
|
fileMimeType?: string
|
||||||
|
fileSha256?: string
|
||||||
|
fileMetadata?: Record<string, unknown>
|
||||||
|
idempotencyKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackendPage<T> {
|
||||||
|
records?: T[]
|
||||||
|
items?: T[]
|
||||||
|
content?: T[]
|
||||||
|
page?: number
|
||||||
|
size?: number
|
||||||
|
total?: number
|
||||||
|
pages?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackendCollection<T> = T[] | BackendPage<T>
|
||||||
|
|
||||||
|
export interface RoleRecord {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
color?: string
|
||||||
|
userCount?: number
|
||||||
|
status: RoleStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoleStatus = 'ENABLED' | 'DISABLED' | string
|
||||||
|
|
||||||
|
export interface RoleResponseDto {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
description?: string | null
|
||||||
|
status?: RoleStatus | null
|
||||||
|
userCount?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateRoleRequest {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
status?: RoleStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateRoleRequest = Partial<CreateRoleRequest>
|
||||||
|
|
||||||
|
export interface CampusRecord {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
address?: string
|
||||||
|
status: OrganizationStatus
|
||||||
|
sortNo: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DepartmentRecord {
|
||||||
|
id: string
|
||||||
|
campusId: string
|
||||||
|
parentId?: string | null
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
status: OrganizationStatus
|
||||||
|
sortNo: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrganizationStatus = 'ENABLED' | 'DISABLED' | string
|
||||||
|
|
||||||
|
export interface CreateCampusRequest {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
address?: string
|
||||||
|
status?: OrganizationStatus
|
||||||
|
sortNo?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateCampusRequest = Partial<CreateCampusRequest>
|
||||||
|
|
||||||
|
export interface CreateDepartmentRequest {
|
||||||
|
campusId: string
|
||||||
|
parentId?: string | null
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
status?: OrganizationStatus
|
||||||
|
sortNo?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateDepartmentRequest = Partial<CreateDepartmentRequest>
|
||||||
|
|
||||||
|
export interface OrganizationResponseDto {
|
||||||
|
id: string
|
||||||
|
code?: string | null
|
||||||
|
name?: string | null
|
||||||
|
address?: string | null
|
||||||
|
status?: OrganizationStatus | null
|
||||||
|
sortNo?: number | null
|
||||||
|
campusId?: string | null
|
||||||
|
parentId?: string | null
|
||||||
|
campusName?: string | null
|
||||||
|
departmentId?: string | null
|
||||||
|
departmentName?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export type ReportPeriod = 'today' | 'week' | 'month' | 'year'
|
export type ReportPeriod = 'today' | 'week' | 'month' | 'year'
|
||||||
|
|
||||||
export interface ReportQuery {
|
export interface ReportQuery {
|
||||||
@@ -70,10 +253,62 @@ export interface UserRecord {
|
|||||||
role: string
|
role: string
|
||||||
status: UserStatus
|
status: UserStatus
|
||||||
lastLoginAt: string
|
lastLoginAt: string
|
||||||
|
employeeNo?: string | null
|
||||||
|
phone?: string | null
|
||||||
|
email?: string | null
|
||||||
|
campusId?: string | null
|
||||||
|
campusName?: string
|
||||||
|
departmentId?: string | null
|
||||||
|
departmentName?: string
|
||||||
|
roleNames?: string[]
|
||||||
|
dataScope?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserListResponse = PageResult<UserRecord>
|
export type UserListResponse = PageResult<UserRecord>
|
||||||
|
|
||||||
|
export interface UserResponseDto {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
displayName: string
|
||||||
|
employeeNo?: string | null
|
||||||
|
phone?: string | null
|
||||||
|
email?: string | null
|
||||||
|
campusId?: string | null
|
||||||
|
departmentId?: string | null
|
||||||
|
dataScope?: string | null
|
||||||
|
status?: string | null
|
||||||
|
lastLoginAt?: string | null
|
||||||
|
createdAt?: string | null
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUserRequest {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
displayName: string
|
||||||
|
employeeNo?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
campusId: string
|
||||||
|
departmentId?: string | null
|
||||||
|
status?: BackendUserStatus
|
||||||
|
dataScope?: BackendDataScope
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackendUserStatus = 'ENABLED' | 'DISABLED' | string
|
||||||
|
export type BackendDataScope = 'ALL' | 'CAMPUS' | 'DEPARTMENT' | 'READ_ONLY_ALL' | string
|
||||||
|
|
||||||
|
export interface UpdateUserRequest {
|
||||||
|
displayName: string
|
||||||
|
employeeNo?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
campusId: string
|
||||||
|
departmentId?: string | null
|
||||||
|
status: BackendUserStatus
|
||||||
|
dataScope: BackendDataScope
|
||||||
|
}
|
||||||
|
|
||||||
export type PermissionSubjectType = 'user' | 'role'
|
export type PermissionSubjectType = 'user' | 'role'
|
||||||
|
|
||||||
export interface PermissionQuery extends PageQuery {
|
export interface PermissionQuery extends PageQuery {
|
||||||
@@ -115,3 +350,67 @@ export interface SystemSettingsResponse {
|
|||||||
export interface UpdateSystemSettingsRequest {
|
export interface UpdateSystemSettingsRequest {
|
||||||
values: Record<string, SettingValue>
|
values: Record<string, SettingValue>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApiPermissionSubjectType = 'ROLE' | 'DEPARTMENT' | 'USER'
|
||||||
|
export type ApiPermissionLevel = 'VIEW' | 'USE' | 'MAINTAIN'
|
||||||
|
export type ApiPermissionEffect = 'ALLOW' | 'DENY'
|
||||||
|
|
||||||
|
export interface TemplatePermissionResponseDto {
|
||||||
|
id: string
|
||||||
|
templateId: string
|
||||||
|
subjectType: ApiPermissionSubjectType
|
||||||
|
subjectId: string
|
||||||
|
permissionLevel: ApiPermissionLevel
|
||||||
|
effect: ApiPermissionEffect
|
||||||
|
inherited: boolean
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplatePermissionRecord {
|
||||||
|
id: string
|
||||||
|
templateId: string
|
||||||
|
subjectType: ApiPermissionSubjectType
|
||||||
|
subjectId: string
|
||||||
|
subjectName: string
|
||||||
|
permissionLevel: ApiPermissionLevel
|
||||||
|
effect: ApiPermissionEffect
|
||||||
|
inherited: boolean
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTemplatePermissionRequest {
|
||||||
|
subjectType: ApiPermissionSubjectType
|
||||||
|
subjectId: string
|
||||||
|
permissionLevel: ApiPermissionLevel
|
||||||
|
effect: ApiPermissionEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogQuery extends PageQuery {
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogResponseDto {
|
||||||
|
id: string
|
||||||
|
action: string
|
||||||
|
resourceType: string
|
||||||
|
resourceId: string
|
||||||
|
operatorId: string
|
||||||
|
clientIp?: string | null
|
||||||
|
userAgent?: string | null
|
||||||
|
detailsJson?: string | null
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogRecord {
|
||||||
|
id: string
|
||||||
|
action: string
|
||||||
|
resourceType: string
|
||||||
|
resourceId: string
|
||||||
|
operatorId: string
|
||||||
|
clientIp: string
|
||||||
|
userAgent: string
|
||||||
|
detailsJson: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuditLogListResponse = PageResult<AuditLogRecord>
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
import { request } from '@/utils/request'
|
import axios from 'axios'
|
||||||
|
|
||||||
import type { UserListResponse, UserQuery, UserRecord } from './types'
|
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
import type { ApiResponse, PageResult } from '@/types/common'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
BackendCollection,
|
||||||
|
BackendPage,
|
||||||
|
CreateUserRequest,
|
||||||
|
UpdateUserRequest,
|
||||||
|
UserListResponse,
|
||||||
|
UserQuery,
|
||||||
|
UserRecord,
|
||||||
|
UserResponseDto,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||||
|
|
||||||
|
export const isUserMockEnabled = useMockData
|
||||||
|
|
||||||
const mockRecords: UserRecord[] = [
|
const mockRecords: UserRecord[] = [
|
||||||
{
|
{
|
||||||
id: 'user-001',
|
id: 'user-001',
|
||||||
@@ -34,10 +49,89 @@ const mockRecords: UserRecord[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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 normalizeStatus(value: string | null | undefined): UserRecord['status'] {
|
||||||
|
return value === 'DISABLED' || value === 'INACTIVE' ? 'disabled' : 'enabled'
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapUser(dto: UserResponseDto): UserRecord {
|
||||||
|
return {
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.displayName,
|
||||||
|
account: dto.username,
|
||||||
|
department: dto.departmentId ?? '未指定科室',
|
||||||
|
role: '未分配角色',
|
||||||
|
status: normalizeStatus(dto.status),
|
||||||
|
lastLoginAt: formatDateTime(dto.lastLoginAt),
|
||||||
|
employeeNo: dto.employeeNo,
|
||||||
|
phone: dto.phone,
|
||||||
|
email: dto.email,
|
||||||
|
campusId: dto.campusId,
|
||||||
|
departmentId: dto.departmentId,
|
||||||
|
dataScope: dto.dataScope ?? '—',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePage<T>(
|
||||||
|
data: BackendPage<T> | T[],
|
||||||
|
fallbackPage: number,
|
||||||
|
fallbackPageSize: number,
|
||||||
|
): PageResult<T> {
|
||||||
|
const records = Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
records,
|
||||||
|
total: Array.isArray(data) ? records.length : (data.total ?? records.length),
|
||||||
|
page: Array.isArray(data) ? fallbackPage : (data.page ?? fallbackPage),
|
||||||
|
pageSize: Array.isArray(data) ? fallbackPageSize : (data.size ?? fallbackPageSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBackendQuery(query: UserQuery) {
|
||||||
|
const params: Record<string, string | number> = {
|
||||||
|
page: Math.max(query.page, 1),
|
||||||
|
size: Math.min(Math.max(query.pageSize, 1), 200),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.keyword?.trim()) {
|
||||||
|
params.keyword = query.keyword.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
export function getUsers(query: UserQuery): Promise<UserListResponse> {
|
export function getUsers(query: UserQuery): Promise<UserListResponse> {
|
||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
return request.get<UserListResponse>('/management/users', {
|
return request
|
||||||
params: query,
|
.get<ApiResponse<BackendCollection<UserResponseDto>>>('/v1/users', {
|
||||||
|
params: toBackendQuery(query),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const page = normalizePage(unwrapApiResponse(response), query.page, query.pageSize)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...page,
|
||||||
|
records: page.records.map(mapUser),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,3 +155,57 @@ export function getUsers(query: UserQuery): Promise<UserListResponse> {
|
|||||||
pageSize,
|
pageSize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserDetail(id: string): Promise<UserRecord | null> {
|
||||||
|
if (useMockData) {
|
||||||
|
const record = mockRecords.find((item) => item.id === id)
|
||||||
|
return record ? { ...record } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request.get<ApiResponse<UserResponseDto>>(
|
||||||
|
`/v1/users/${encodeURIComponent(id)}`,
|
||||||
|
)
|
||||||
|
return mapUser(unwrapApiResponse(response))
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createUser(payload: CreateUserRequest): Promise<UserRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行用户写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post<ApiResponse<UserResponseDto>>('/v1/users', payload)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then(mapUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUser(id: string, payload: UpdateUserRequest): Promise<UserRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行用户写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.put<ApiResponse<UserResponseDto>>(`/v1/users/${encodeURIComponent(id)}`, payload)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then(mapUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteUser(id: string): Promise<void> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行用户写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.delete<ApiResponse<null>>(`/v1/users/${encodeURIComponent(id)}`)
|
||||||
|
.then((response) => {
|
||||||
|
unwrapNullableApiResponse(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
121
clinical-web/src/api/workbench/artifacts.ts
Normal file
121
clinical-web/src/api/workbench/artifacts.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { unwrapApiResponse } from '@/utils/api-response'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiPageResponse,
|
||||||
|
ApiResponseOf,
|
||||||
|
SignArtifactResponseDto,
|
||||||
|
SignArtifactType,
|
||||||
|
SigningArtifact,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||||
|
|
||||||
|
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 ?? dto.type ?? 'UNKNOWN'
|
||||||
|
const label = artifactLabels[artifactType] ?? '签署文件'
|
||||||
|
const mimeType = dto.mimeType ?? dto.contentType ?? 'application/octet-stream'
|
||||||
|
const extension =
|
||||||
|
mimeType === 'application/pdf' ? 'pdf' : mimeType === 'image/png' ? 'png' : 'bin'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: dto.id,
|
||||||
|
taskId: dto.taskId,
|
||||||
|
artifactType,
|
||||||
|
label,
|
||||||
|
fileName: dto.fileName || `${label}.${extension}`,
|
||||||
|
mimeType,
|
||||||
|
size: dto.size ?? dto.byteSize ?? dto.fileSize ?? 0,
|
||||||
|
sha256: dto.sha256 ?? dto.contentSha256 ?? '',
|
||||||
|
createdAt: formatDateTime(dto.createdAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isArtifactMockEnabled = useMockData
|
||||||
|
|
||||||
|
export async function getSigningArtifacts(taskId: string): Promise<SigningArtifact[]> {
|
||||||
|
if (useMockData) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return Promise.resolve(new Blob(['Mock 签署文件,仅用于界面演示。'], { type: 'text/plain' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.get<Blob>(`/v1/sign-artifacts/${encodeURIComponent(artifactId)}/download`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
})
|
||||||
|
}
|
||||||
152
clinical-web/src/api/workbench/deliveries.ts
Normal file
152
clinical-web/src/api/workbench/deliveries.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiResponseOf,
|
||||||
|
PadBindingRequest,
|
||||||
|
PadSessionUploadInput,
|
||||||
|
SendSmsDeliveryRequest,
|
||||||
|
SignDeliveryResponseDto,
|
||||||
|
SignatureUploadResponse,
|
||||||
|
SigningTokenUploadInput,
|
||||||
|
TokenConsumeRequest,
|
||||||
|
TokenConsumeResponse,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
function createIdempotencyKey() {
|
||||||
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||||
|
return `clinical-web-${crypto.randomUUID()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `clinical-web-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function idempotencyHeaders(value?: string) {
|
||||||
|
return { 'Idempotency-Key': value || createIdempotencyKey() }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendSigningSms(
|
||||||
|
taskId: string,
|
||||||
|
payload: SendSmsDeliveryRequest,
|
||||||
|
idempotencyKey?: string,
|
||||||
|
): Promise<SignDeliveryResponseDto | null> {
|
||||||
|
return request
|
||||||
|
.post<ApiResponseOf<SignDeliveryResponseDto | null>>(
|
||||||
|
`/v1/sign-deliveries/${encodeURIComponent(taskId)}/sms/send`,
|
||||||
|
payload,
|
||||||
|
{ headers: idempotencyHeaders(idempotencyKey) },
|
||||||
|
)
|
||||||
|
.then(unwrapNullableApiResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resendSigningSms(
|
||||||
|
taskId: string,
|
||||||
|
expectedRowVersion?: number,
|
||||||
|
idempotencyKey?: string,
|
||||||
|
): Promise<SignDeliveryResponseDto | null> {
|
||||||
|
const data = expectedRowVersion === undefined ? undefined : { expectedRowVersion }
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post<ApiResponseOf<SignDeliveryResponseDto | null>>(
|
||||||
|
`/v1/sign-deliveries/${encodeURIComponent(taskId)}/sms/resend`,
|
||||||
|
data,
|
||||||
|
{ headers: idempotencyHeaders(idempotencyKey) },
|
||||||
|
)
|
||||||
|
.then(unwrapNullableApiResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPadSigningSession(
|
||||||
|
taskId: string,
|
||||||
|
payload: PadBindingRequest = {},
|
||||||
|
idempotencyKey?: string,
|
||||||
|
): Promise<SignDeliveryResponseDto> {
|
||||||
|
return request
|
||||||
|
.post<ApiResponseOf<SignDeliveryResponseDto>>(
|
||||||
|
`/v1/sign-deliveries/${encodeURIComponent(taskId)}/pad/sessions`,
|
||||||
|
payload,
|
||||||
|
{ headers: idempotencyHeaders(idempotencyKey) },
|
||||||
|
)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumeSigningToken(payload: TokenConsumeRequest): Promise<TokenConsumeResponse> {
|
||||||
|
return request
|
||||||
|
.post<ApiResponseOf<TokenConsumeResponse>>('/v1/sign-deliveries/token/consume', payload)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSignatureFormData(file: File | Blob, metadata: string) {
|
||||||
|
const formData = new FormData()
|
||||||
|
const fileName =
|
||||||
|
typeof File !== 'undefined' && file instanceof File && file.name ? file.name : 'signature.png'
|
||||||
|
formData.append('file', file, fileName)
|
||||||
|
formData.append('metadata', metadata)
|
||||||
|
return formData
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadSigningSignature(
|
||||||
|
payload: SigningTokenUploadInput,
|
||||||
|
): Promise<SignatureUploadResponse> {
|
||||||
|
const formData = createSignatureFormData(payload.file, payload.metadata)
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post<ApiResponseOf<SignatureUploadResponse>>(
|
||||||
|
`/v1/sign-deliveries/${encodeURIComponent(payload.taskId)}/signature`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
deliveryId: payload.deliveryId,
|
||||||
|
uploadToken: payload.uploadToken,
|
||||||
|
},
|
||||||
|
headers: idempotencyHeaders(payload.idempotencyKey),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PAD 专用兼容路径。服务端要求同时提交 PAD 元数据和真实 PNG,旧的 JSON-only 提交会返回 409。
|
||||||
|
* 具体的 challenge、文件摘要和 storageKey 由设备适配层生成,页面不应伪造这些值。
|
||||||
|
*/
|
||||||
|
export function uploadPadSigningSignature(
|
||||||
|
payload: PadSessionUploadInput,
|
||||||
|
): Promise<SignatureUploadResponse> {
|
||||||
|
if (!payload.pad) {
|
||||||
|
return Promise.reject(new Error('PAD 上传缺少会话元数据'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = createSignatureFormData(payload.file, payload.metadata)
|
||||||
|
formData.append('challenge', payload.pad.challenge)
|
||||||
|
formData.append('contentType', payload.pad.contentType)
|
||||||
|
formData.append('sizeBytes', String(payload.pad.sizeBytes ?? payload.file.size))
|
||||||
|
formData.append('sha256', payload.pad.sha256)
|
||||||
|
formData.append('storageKey', payload.pad.storageKey)
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post<ApiResponseOf<SignatureUploadResponse>>(
|
||||||
|
`/v1/sign-deliveries/${encodeURIComponent(payload.taskId)}/pad/sessions/${encodeURIComponent(payload.deliveryId)}/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
params: { uploadToken: payload.uploadToken },
|
||||||
|
headers: idempotencyHeaders(payload.idempotencyKey),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadSigningSignatureByProof(
|
||||||
|
payload: SigningTokenUploadInput,
|
||||||
|
): Promise<SignatureUploadResponse> {
|
||||||
|
const formData = createSignatureFormData(payload.file, payload.metadata)
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post<ApiResponseOf<SignatureUploadResponse>>('/v1/sign-deliveries/token/signature', formData, {
|
||||||
|
params: {
|
||||||
|
taskId: payload.taskId,
|
||||||
|
deliveryId: payload.deliveryId,
|
||||||
|
uploadToken: payload.uploadToken,
|
||||||
|
},
|
||||||
|
headers: idempotencyHeaders(payload.idempotencyKey),
|
||||||
|
})
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { request } from '@/utils/request'
|
import { getCampuses } from '@/api/management/organization'
|
||||||
|
|
||||||
|
import { getSigningTasks, getSigningTemplates } from './signing'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
HomeRankingPeriod,
|
HomeRankingPeriod,
|
||||||
@@ -6,6 +8,7 @@ import type {
|
|||||||
WorkbenchDocumentRanking,
|
WorkbenchDocumentRanking,
|
||||||
WorkbenchOverviewQuery,
|
WorkbenchOverviewQuery,
|
||||||
WorkbenchOverviewResponse,
|
WorkbenchOverviewResponse,
|
||||||
|
SigningTaskRecord,
|
||||||
WorkbenchTodoTask,
|
WorkbenchTodoTask,
|
||||||
WorkbenchTrendPoint,
|
WorkbenchTrendPoint,
|
||||||
} from './types'
|
} from './types'
|
||||||
@@ -243,6 +246,107 @@ function createRanking(
|
|||||||
.sort((left, right) => right.signedCount - left.signedCount)
|
.sort((left, right) => right.signedCount - left.signedCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWorkbenchCampus(value: string): value is WorkbenchCampus {
|
||||||
|
return value === '本部院区' || value === '东院区' || value === '西院区'
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDate(value: string | undefined) {
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value.includes('T') ? value : value.replace(' ', 'T'))
|
||||||
|
return Number.isNaN(date.getTime()) ? null : date
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameDay(left: Date | null, right: Date) {
|
||||||
|
return Boolean(
|
||||||
|
left &&
|
||||||
|
left.getFullYear() === right.getFullYear() &&
|
||||||
|
left.getMonth() === right.getMonth() &&
|
||||||
|
left.getDate() === right.getDate(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTaskDate(task: SigningTaskRecord) {
|
||||||
|
return parseDate(task.createdAt) ?? parseDate(task.updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPendingTask(task: SigningTaskRecord) {
|
||||||
|
return task.status === 'pending' || task.status === 'signing'
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLiveOverview(
|
||||||
|
tasks: SigningTaskRecord[],
|
||||||
|
templateCount: number,
|
||||||
|
coveredDepartments: number,
|
||||||
|
rankingPeriod: HomeRankingPeriod,
|
||||||
|
): WorkbenchOverviewResponse {
|
||||||
|
const today = new Date()
|
||||||
|
const signedTasks = tasks.filter((task) => task.status === 'signed')
|
||||||
|
const todaySigned = signedTasks.filter((task) => isSameDay(getTaskDate(task), today)).length
|
||||||
|
const pendingTasks = tasks.filter(isPendingTask)
|
||||||
|
const todayOverdue = tasks.filter(
|
||||||
|
(task) => task.status === 'expired' && isSameDay(getTaskDate(task), today),
|
||||||
|
).length
|
||||||
|
|
||||||
|
const trend = Array.from({ length: rankingPeriod }, (_, index) => {
|
||||||
|
const date = new Date(today)
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
date.setDate(today.getDate() - rankingPeriod + index + 1)
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: `${date.getMonth() + 1}/${date.getDate()}`,
|
||||||
|
value: signedTasks.filter((task) => isSameDay(getTaskDate(task), date)).length,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const todos = tasks
|
||||||
|
.filter((task) => isPendingTask(task) || task.status === 'expired')
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftDate = getTaskDate(left)?.getTime() ?? 0
|
||||||
|
const rightDate = getTaskDate(right)?.getTime() ?? 0
|
||||||
|
return rightDate - leftDate
|
||||||
|
})
|
||||||
|
.slice(0, 8)
|
||||||
|
.map((task) => ({
|
||||||
|
id: task.id,
|
||||||
|
patientId: task.patientId,
|
||||||
|
patientName: task.patientName,
|
||||||
|
documentName: task.documentName,
|
||||||
|
department: task.department,
|
||||||
|
status: task.status === 'expired' ? ('overdue' as const) : ('pending' as const),
|
||||||
|
method: task.method,
|
||||||
|
updatedAt: task.updatedAt,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const rankingMap = new Map<string, WorkbenchDocumentRanking>()
|
||||||
|
signedTasks.forEach((task) => {
|
||||||
|
const current = rankingMap.get(task.documentName)
|
||||||
|
rankingMap.set(task.documentName, {
|
||||||
|
id: current?.id ?? task.documentId,
|
||||||
|
documentName: task.documentName,
|
||||||
|
department: task.department,
|
||||||
|
signedCount: (current?.signedCount ?? 0) + 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: {
|
||||||
|
todaySigned,
|
||||||
|
pendingPatientSigning: pendingTasks.length,
|
||||||
|
todayOverdue,
|
||||||
|
availableTemplates: templateCount,
|
||||||
|
coveredDepartments,
|
||||||
|
},
|
||||||
|
trend,
|
||||||
|
todos,
|
||||||
|
documentRanking: [...rankingMap.values()]
|
||||||
|
.sort((left, right) => right.signedCount - left.signedCount)
|
||||||
|
.slice(0, 10),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getWorkbenchOverview(
|
export function getWorkbenchOverview(
|
||||||
query: WorkbenchOverviewQuery,
|
query: WorkbenchOverviewQuery,
|
||||||
): Promise<WorkbenchOverviewResponse> {
|
): Promise<WorkbenchOverviewResponse> {
|
||||||
@@ -257,7 +361,38 @@ export function getWorkbenchOverview(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return request.get<WorkbenchOverviewResponse>('/workbench/overview', {
|
return Promise.allSettled([getSigningTemplates(), getCampuses()]).then(
|
||||||
params: query,
|
async ([templatesResult, campusesResult]) => {
|
||||||
})
|
const templates = templatesResult.status === 'fulfilled' ? templatesResult.value : []
|
||||||
|
const campusNames: Record<string, WorkbenchCampus> =
|
||||||
|
campusesResult.status === 'fulfilled'
|
||||||
|
? (Object.fromEntries(
|
||||||
|
campusesResult.value
|
||||||
|
.filter((campus) => isWorkbenchCampus(campus.name))
|
||||||
|
.map((campus) => [campus.id, campus.name]),
|
||||||
|
) as Record<string, WorkbenchCampus>)
|
||||||
|
: {}
|
||||||
|
const tasksResponse = await getSigningTasks(
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
status: 'all',
|
||||||
|
dateRange: 'all',
|
||||||
|
campus: 'all',
|
||||||
|
method: 'all',
|
||||||
|
},
|
||||||
|
{ templates, campusNames },
|
||||||
|
)
|
||||||
|
const scopedTasks = Object.keys(campusNames).length
|
||||||
|
? tasksResponse.records.filter((task) => task.campus === query.campus)
|
||||||
|
: tasksResponse.records
|
||||||
|
|
||||||
|
return createLiveOverview(
|
||||||
|
scopedTasks,
|
||||||
|
templates.length,
|
||||||
|
new Set(templates.map((template) => template.department)).size,
|
||||||
|
query.rankingPeriod,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { unwrapApiResponse } from '@/utils/api-response'
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
|
import { resendSigningSms as resendSigningSmsDelivery } from './deliveries'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiPageResponse,
|
ApiPageResponse,
|
||||||
@@ -368,22 +370,6 @@ function addHours(value: string, hours: number) {
|
|||||||
|
|
||||||
export const isSigningMockEnabled = useMockData
|
export const isSigningMockEnabled = useMockData
|
||||||
|
|
||||||
function isSuccessCode(code: number | string) {
|
|
||||||
return code === 0 || code === '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapApiResponse<T>(response: ApiResponseOf<T>): T {
|
|
||||||
if (!isSuccessCode(response.code)) {
|
|
||||||
throw new Error(response.message || '接口请求失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data === null) {
|
|
||||||
throw new Error(response.message || '接口未返回业务数据')
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNotFoundError(error: unknown) {
|
function isNotFoundError(error: unknown) {
|
||||||
return axios.isAxiosError(error) && error.response?.status === 404
|
return axios.isAxiosError(error) && error.response?.status === 404
|
||||||
}
|
}
|
||||||
@@ -521,7 +507,8 @@ function mapSigningTask(
|
|||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
templateVersionId: dto.templateVersionId,
|
templateVersionId: dto.templateVersionId,
|
||||||
campus: options.campus ?? '本部院区',
|
campus: options.campusNames?.[dto.campusId] ?? options.campus ?? '本部院区',
|
||||||
|
campusId: dto.campusId,
|
||||||
patientId: dto.patientSnapshot.patientId,
|
patientId: dto.patientSnapshot.patientId,
|
||||||
patientName: dto.patientSnapshot.name,
|
patientName: dto.patientSnapshot.name,
|
||||||
sex: normalizePatientSex(dto.patientSnapshot.sex),
|
sex: normalizePatientSex(dto.patientSnapshot.sex),
|
||||||
@@ -532,11 +519,13 @@ function mapSigningTask(
|
|||||||
documentId: template?.id ?? dto.templateVersionId,
|
documentId: template?.id ?? dto.templateVersionId,
|
||||||
documentName: template?.name ?? `模板版本 ${dto.templateVersionNumber}`,
|
documentName: template?.name ?? `模板版本 ${dto.templateVersionNumber}`,
|
||||||
department: dto.visitSnapshot.departmentName || '未指定科室',
|
department: dto.visitSnapshot.departmentName || '未指定科室',
|
||||||
|
departmentId: dto.departmentId,
|
||||||
signerName: dto.patientSnapshot.name,
|
signerName: dto.patientSnapshot.name,
|
||||||
method: normalizeSigningMethod(dto.signMethod),
|
method: normalizeSigningMethod(dto.signMethod),
|
||||||
status: normalizeTaskStatus(dto.status),
|
status: normalizeTaskStatus(dto.status),
|
||||||
source: dto.visitSnapshot.sourceSystem || dto.patientSnapshot.sourceSystem || '—',
|
source: dto.visitSnapshot.sourceSystem || dto.patientSnapshot.sourceSystem || '—',
|
||||||
expiresAt: formatApiDateTime(dto.expiredAt),
|
expiresAt: formatApiDateTime(dto.expiredAt),
|
||||||
|
createdAt: formatApiDateTime(dto.createdAt),
|
||||||
updatedAt: formatApiDateTime(dto.updatedAt),
|
updatedAt: formatApiDateTime(dto.updatedAt),
|
||||||
rowVersion: dto.rowVersion,
|
rowVersion: dto.rowVersion,
|
||||||
backendStatus: dto.status,
|
backendStatus: dto.status,
|
||||||
@@ -559,7 +548,7 @@ function createTemplateFromInput(input: CreateSigningTaskInput): SigningTemplate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBackendTaskQuery(query: SigningTaskQuery, templates?: SigningTemplate[]) {
|
function toBackendTaskQuery(query: SigningTaskQuery, options: SigningApiOptions = {}) {
|
||||||
const params: Record<string, string | number> = {
|
const params: Record<string, string | number> = {
|
||||||
page: Math.max(query.page, 1),
|
page: Math.max(query.page, 1),
|
||||||
size: Math.min(Math.max(query.pageSize, 1), 200),
|
size: Math.min(Math.max(query.pageSize, 1), 200),
|
||||||
@@ -592,7 +581,7 @@ function toBackendTaskQuery(query: SigningTaskQuery, templates?: SigningTemplate
|
|||||||
|
|
||||||
if (query.documentId) {
|
if (query.documentId) {
|
||||||
params.templateVersionId =
|
params.templateVersionId =
|
||||||
findTemplate(templates, query.documentId)?.versionId ?? query.documentId
|
findTemplate(options.templates, query.documentId)?.versionId ?? query.documentId
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -615,8 +604,14 @@ function toBackendTaskQuery(query: SigningTaskQuery, templates?: SigningTemplate
|
|||||||
params.createdTo = end.toISOString()
|
params.createdTo = end.toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 院区、科室和就诊类型在页面中使用展示名称,不能直接当作 UUID 发送。
|
if (query.campus && query.campus !== 'all' && options.campusId) {
|
||||||
// 后端会按当前登录用户的数据范围默认过滤,待院区/科室字典接入后再补充 ID 映射。
|
params.campusId = options.campusId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.department && options.departmentIds?.[query.department]) {
|
||||||
|
params.departmentId = options.departmentIds[query.department]
|
||||||
|
}
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,7 +670,7 @@ export async function getSigningTasks(
|
|||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
const response = await request.get<ApiResponseOf<ApiPageResponse<SignTaskResponseDto>>>(
|
const response = await request.get<ApiResponseOf<ApiPageResponse<SignTaskResponseDto>>>(
|
||||||
'/v1/sign-tasks',
|
'/v1/sign-tasks',
|
||||||
{ params: toBackendTaskQuery(query, options.templates) },
|
{ params: toBackendTaskQuery(query, options) },
|
||||||
)
|
)
|
||||||
const page = unwrapApiResponse(response)
|
const page = unwrapApiResponse(response)
|
||||||
|
|
||||||
@@ -853,12 +848,15 @@ export async function getSigningTemplates(
|
|||||||
} = {},
|
} = {},
|
||||||
): Promise<SigningTemplate[]> {
|
): Promise<SigningTemplate[]> {
|
||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
const response = await request.get<ApiResponseOf<AvailableTemplateVersionResponseDto[]>>(
|
const response = await request.get<
|
||||||
'/v1/templates/available-versions',
|
ApiResponseOf<
|
||||||
{ params: query },
|
AvailableTemplateVersionResponseDto[] | ApiPageResponse<AvailableTemplateVersionResponseDto>
|
||||||
)
|
>
|
||||||
|
>('/v1/templates/available-versions', { params: query })
|
||||||
|
|
||||||
return unwrapApiResponse(response).map(mapAvailableTemplate)
|
const data = unwrapApiResponse(response)
|
||||||
|
const records = Array.isArray(data) ? data : data.records
|
||||||
|
return records.map(mapAvailableTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(mockTemplates.map((template) => ({ ...template })))
|
return Promise.resolve(mockTemplates.map((template) => ({ ...template })))
|
||||||
@@ -1033,11 +1031,7 @@ export async function resendSigningSms(
|
|||||||
options: SigningTaskActionOptions = {},
|
options: SigningTaskActionOptions = {},
|
||||||
): Promise<SigningTaskRecord | null> {
|
): Promise<SigningTaskRecord | null> {
|
||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
const response = await request.post<ApiResponseOf<SignTaskSendPreparationResponseDto>>(
|
await resendSigningSmsDelivery(id, options.expectedRowVersion)
|
||||||
`/v1/sign-tasks/${encodeURIComponent(id)}/resend`,
|
|
||||||
getTaskActionBody(options.expectedRowVersion),
|
|
||||||
)
|
|
||||||
unwrapApiResponse(response)
|
|
||||||
return getSigningTaskDetail(id, options)
|
return getSigningTaskDetail(id, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,14 +122,17 @@ export type SigningDateRange = 'today' | 'yesterday' | '3d' | '7d' | 'all'
|
|||||||
export interface SigningTaskRecord extends WorkbenchTask {
|
export interface SigningTaskRecord extends WorkbenchTask {
|
||||||
templateVersionId: string
|
templateVersionId: string
|
||||||
campus: WorkbenchCampus
|
campus: WorkbenchCampus
|
||||||
|
campusId?: string
|
||||||
patientId: string
|
patientId: string
|
||||||
sex: PatientSex
|
sex: PatientSex
|
||||||
age: number
|
age: number
|
||||||
documentId: string
|
documentId: string
|
||||||
|
departmentId?: string | null
|
||||||
signerName: string
|
signerName: string
|
||||||
method: SigningMethod
|
method: SigningMethod
|
||||||
source: string
|
source: string
|
||||||
expiresAt: string
|
expiresAt: string
|
||||||
|
createdAt?: string
|
||||||
visitType: VisitType
|
visitType: VisitType
|
||||||
visitDate: string
|
visitDate: string
|
||||||
rowVersion?: number
|
rowVersion?: number
|
||||||
@@ -167,6 +170,9 @@ export interface CreateSigningTaskRequest {
|
|||||||
export interface SigningApiOptions {
|
export interface SigningApiOptions {
|
||||||
templates?: SigningTemplate[]
|
templates?: SigningTemplate[]
|
||||||
campus?: WorkbenchCampus
|
campus?: WorkbenchCampus
|
||||||
|
campusId?: string
|
||||||
|
campusNames?: Record<string, WorkbenchCampus>
|
||||||
|
departmentIds?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SigningTaskActionOptions extends SigningApiOptions {
|
export interface SigningTaskActionOptions extends SigningApiOptions {
|
||||||
@@ -301,6 +307,113 @@ export interface SignTaskSendPreparationResponseDto {
|
|||||||
rowVersion: number
|
rowVersion: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SendSmsDeliveryRequest {
|
||||||
|
destination: string
|
||||||
|
templateCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SignDeliveryChannel = 'PAD' | 'SMS'
|
||||||
|
|
||||||
|
export type SignDeliveryStatus =
|
||||||
|
| 'PENDING'
|
||||||
|
| 'IN_FLIGHT'
|
||||||
|
| 'SUCCEEDED'
|
||||||
|
| 'RETRYABLE'
|
||||||
|
| 'FAILED'
|
||||||
|
| 'DEAD'
|
||||||
|
| 'CANCELLED'
|
||||||
|
| 'UNKNOWN'
|
||||||
|
| 'REVOKED'
|
||||||
|
| 'EXPIRED'
|
||||||
|
| string
|
||||||
|
|
||||||
|
export interface SignDeliveryResponseDto {
|
||||||
|
deliveryId: string
|
||||||
|
taskId: string
|
||||||
|
channel: SignDeliveryChannel
|
||||||
|
status: SignDeliveryStatus
|
||||||
|
attemptId?: string | null
|
||||||
|
attemptNo?: number | null
|
||||||
|
tokenVersion?: number | null
|
||||||
|
destinationMasked?: string | null
|
||||||
|
token?: string | null
|
||||||
|
tokenExpiresAt?: string | null
|
||||||
|
errorCode?: string | null
|
||||||
|
errorMessage?: string | null
|
||||||
|
traceId?: string | null
|
||||||
|
timestamp?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PadBindingRequest {
|
||||||
|
deviceId?: string
|
||||||
|
expiresAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PadUploadRequest {
|
||||||
|
challenge: string
|
||||||
|
contentType: 'image/png'
|
||||||
|
sizeBytes?: number
|
||||||
|
sha256: string
|
||||||
|
storageKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenConsumeRequest {
|
||||||
|
token: string
|
||||||
|
channel?: SignDeliveryChannel
|
||||||
|
taskId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenConsumeResponse {
|
||||||
|
deliveryId: string
|
||||||
|
taskId: string
|
||||||
|
channel: SignDeliveryChannel
|
||||||
|
uploadToken: string
|
||||||
|
traceId?: string | null
|
||||||
|
timestamp?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SignatureUploadStatus = BackendSigningTaskStatus
|
||||||
|
|
||||||
|
export interface SignatureUploadResponse {
|
||||||
|
taskId: string
|
||||||
|
pipelineId: string
|
||||||
|
status: SignatureUploadStatus
|
||||||
|
originalPdfArtifactId: string
|
||||||
|
signedPdfArtifactId: string
|
||||||
|
signatureImageArtifactId: string
|
||||||
|
originalPdfSha256: string
|
||||||
|
signedPdfSha256: string
|
||||||
|
signatureImageSha256: string
|
||||||
|
completedAt: string | null
|
||||||
|
traceId?: string | null
|
||||||
|
timestamp?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignatureUploadInput {
|
||||||
|
deliveryId: string
|
||||||
|
uploadToken: string
|
||||||
|
file: File | Blob
|
||||||
|
metadata: string
|
||||||
|
pad?: PadUploadRequest
|
||||||
|
idempotencyKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SigningTokenUploadInput extends Omit<SignatureUploadInput, 'pad'> {
|
||||||
|
taskId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PadSessionUploadInput extends SignatureUploadInput {
|
||||||
|
taskId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignatureDeliveryResult {
|
||||||
|
taskId: string
|
||||||
|
deliveryId: string
|
||||||
|
channel: SignDeliveryChannel
|
||||||
|
uploadToken: string
|
||||||
|
upload: SignatureUploadResponse
|
||||||
|
}
|
||||||
|
|
||||||
export interface SignTaskEventResponseDto {
|
export interface SignTaskEventResponseDto {
|
||||||
id: string
|
id: string
|
||||||
taskId: string
|
taskId: string
|
||||||
@@ -323,6 +436,37 @@ export interface SignTaskEventResponseDto {
|
|||||||
detail: Record<string, unknown> | null
|
detail: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SignArtifactType = 'ORIGINAL_PDF' | 'SIGNATURE_IMAGE' | 'SIGNED_PDF' | string
|
||||||
|
|
||||||
|
/** MEDISIGN 签署产物 DTO。部分文件元数据由不同版本的服务端返回,字段保持可选并在 API 边界归一化。 */
|
||||||
|
export interface SignArtifactResponseDto {
|
||||||
|
id: string
|
||||||
|
taskId: string
|
||||||
|
artifactType?: SignArtifactType
|
||||||
|
type?: SignArtifactType
|
||||||
|
fileName?: string | null
|
||||||
|
mimeType?: string | null
|
||||||
|
contentType?: string | null
|
||||||
|
size?: number | null
|
||||||
|
byteSize?: number | null
|
||||||
|
fileSize?: number | null
|
||||||
|
sha256?: string | null
|
||||||
|
contentSha256?: string | null
|
||||||
|
createdAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SigningArtifact {
|
||||||
|
id: string
|
||||||
|
taskId: string
|
||||||
|
artifactType: SignArtifactType
|
||||||
|
label: string
|
||||||
|
fileName: string
|
||||||
|
mimeType: string
|
||||||
|
size: number
|
||||||
|
sha256: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface SigningTaskEvent {
|
export interface SigningTaskEvent {
|
||||||
id: string
|
id: string
|
||||||
time: string
|
time: string
|
||||||
|
|||||||
Reference in New Issue
Block a user