Compare commits
4 Commits
8b4b9d031d
...
b4184260da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4184260da | ||
|
|
45971b62d9 | ||
|
|
2963c6ea56 | ||
|
|
5b2cc33038 |
@@ -21,6 +21,7 @@ src/
|
|||||||
│ ├─ documents.ts
|
│ ├─ documents.ts
|
||||||
│ ├─ reports.ts
|
│ ├─ reports.ts
|
||||||
│ ├─ users.ts
|
│ ├─ users.ts
|
||||||
|
│ ├─ permissions.ts
|
||||||
│ ├─ document-permissions.ts
|
│ ├─ document-permissions.ts
|
||||||
│ ├─ settings.ts
|
│ ├─ settings.ts
|
||||||
│ └─ types.ts
|
│ └─ types.ts
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench`
|
|||||||
- `api/management/reports.ts`
|
- `api/management/reports.ts`
|
||||||
- `api/management/users.ts`
|
- `api/management/users.ts`
|
||||||
- `api/management/roles.ts`
|
- `api/management/roles.ts`
|
||||||
|
- `api/management/permissions.ts`
|
||||||
- `api/management/organization.ts`
|
- `api/management/organization.ts`
|
||||||
- `api/management/document-permissions.ts`
|
- `api/management/document-permissions.ts`
|
||||||
- `api/management/audit.ts`
|
- `api/management/audit.ts`
|
||||||
@@ -133,6 +134,7 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench`
|
|||||||
所有接口统一使用 `utils/request.ts` 导出的单例请求实例。登录接口已接入 MEDISIGN 后端:
|
所有接口统一使用 `utils/request.ts` 导出的单例请求实例。登录接口已接入 MEDISIGN 后端:
|
||||||
|
|
||||||
- `POST /api/v1/auth/login`:账号密码登录;
|
- `POST /api/v1/auth/login`:账号密码登录;
|
||||||
|
- `GET /api/v1/auth/captcha`:获取按需启用的图形验证码;
|
||||||
- `GET /api/v1/auth/me`:查询当前用户;
|
- `GET /api/v1/auth/me`:查询当前用户;
|
||||||
- `POST /api/v1/auth/logout`:注销当前会话;
|
- `POST /api/v1/auth/logout`:注销当前会话;
|
||||||
- 后续请求自动携带 `X-Token` 请求头。
|
- 后续请求自动携带 `X-Token` 请求头。
|
||||||
@@ -155,6 +157,7 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench`
|
|||||||
- `GET/POST /api/v1/templates`、`GET /api/v1/templates/{id}`:模板列表、详情;
|
- `GET/POST /api/v1/templates`、`GET /api/v1/templates/{id}`:模板列表、详情;
|
||||||
- `GET/POST /api/v1/templates/{id}/versions`、版本工作流接口:版本查询、创建、送审、驳回、通过、发布、停用、归档;
|
- `GET/POST /api/v1/templates/{id}/versions`、版本工作流接口:版本查询、创建、送审、驳回、通过、发布、停用、归档;
|
||||||
- `GET /api/v1/users`、`GET /api/v1/roles`、`GET /api/v1/campuses`、`GET /api/v1/departments`:用户页真实查询及组织字典;
|
- `GET /api/v1/users`、`GET /api/v1/roles`、`GET /api/v1/campuses`、`GET /api/v1/departments`:用户页真实查询及组织字典;
|
||||||
|
- `GET/POST/PUT/DELETE /api/v1/permissions`:API 权限分页查询、详情、新增、修改和停用;
|
||||||
- `GET/POST/DELETE /api/v1/templates/{id}/permissions`:按模板查询和维护文档权限;
|
- `GET/POST/DELETE /api/v1/templates/{id}/permissions`:按模板查询和维护文档权限;
|
||||||
- `GET /api/v1/audit-logs`:全局审计日志查询 API。
|
- `GET /api/v1/audit-logs`:全局审计日志查询 API。
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ApiResponseError, unwrapApiResponse } from '@/utils/api-response'
|
|||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
import type {
|
import type {
|
||||||
AuthenticatedUser,
|
AuthenticatedUser,
|
||||||
|
CaptchaResponse,
|
||||||
CurrentUserResponse,
|
CurrentUserResponse,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
@@ -12,6 +13,11 @@ import type { ApiResponse } from '@/types/common'
|
|||||||
|
|
||||||
export { ApiResponseError } from '@/utils/api-response'
|
export { ApiResponseError } from '@/utils/api-response'
|
||||||
|
|
||||||
|
export async function getCaptcha(): Promise<CaptchaResponse> {
|
||||||
|
const response = await request.get<ApiResponse<CaptchaResponse>>('/v1/auth/captcha')
|
||||||
|
return unwrapApiResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePermissions(value: unknown): string[] {
|
function normalizePermissions(value: unknown): string[] {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.filter((item): item is string => typeof item === 'string')
|
return value.filter((item): item is string => typeof item === 'string')
|
||||||
|
|||||||
209
clinical-web/src/api/management/permissions.ts
Normal file
209
clinical-web/src/api/management/permissions.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
||||||
|
import { request } from '@/utils/request'
|
||||||
|
import type { ApiResponse, PageResult } from '@/types/common'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiPermissionListResponse,
|
||||||
|
ApiPermissionQuery,
|
||||||
|
ApiPermissionRecord,
|
||||||
|
ApiPermissionResponseDto,
|
||||||
|
BackendPage,
|
||||||
|
CreateApiPermissionRequest,
|
||||||
|
UpdateApiPermissionRequest,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
const useMockData = import.meta.env.VITE_USE_MOCK !== 'false'
|
||||||
|
|
||||||
|
export const isApiPermissionMockEnabled = useMockData
|
||||||
|
|
||||||
|
const mockRecords: ApiPermissionRecord[] = [
|
||||||
|
{
|
||||||
|
id: 'api-permission-001',
|
||||||
|
parentId: null,
|
||||||
|
code: 'system:user:query',
|
||||||
|
name: '查询用户',
|
||||||
|
resourceType: 'USER',
|
||||||
|
resourcePath: '/api/v1/users',
|
||||||
|
action: 'QUERY',
|
||||||
|
status: 'ENABLED',
|
||||||
|
createdAt: '2026-08-27 09:00',
|
||||||
|
updatedAt: '2026-08-27 09:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'api-permission-002',
|
||||||
|
parentId: null,
|
||||||
|
code: 'system:sign-task:create',
|
||||||
|
name: '创建签署任务',
|
||||||
|
resourceType: 'SIGN_TASK',
|
||||||
|
resourcePath: '/api/v1/sign-tasks',
|
||||||
|
action: 'CREATE',
|
||||||
|
status: 'ENABLED',
|
||||||
|
createdAt: '2026-08-27 09:02',
|
||||||
|
updatedAt: '2026-08-27 09:02',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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 mapPermission(dto: ApiPermissionResponseDto): ApiPermissionRecord {
|
||||||
|
return {
|
||||||
|
id: dto.id,
|
||||||
|
parentId: dto.parentId ?? null,
|
||||||
|
code: dto.code,
|
||||||
|
name: dto.name,
|
||||||
|
resourceType: dto.resourceType,
|
||||||
|
resourcePath: dto.resourcePath ?? '',
|
||||||
|
action: dto.action ?? 'QUERY',
|
||||||
|
status: dto.status ?? 'ENABLED',
|
||||||
|
createdAt: formatDateTime(dto.createdAt),
|
||||||
|
updatedAt: formatDateTime(dto.updatedAt ?? 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toBackendQuery(query: ApiPermissionQuery) {
|
||||||
|
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 getPermissions(query: ApiPermissionQuery): Promise<ApiPermissionListResponse> {
|
||||||
|
if (!useMockData) {
|
||||||
|
return request
|
||||||
|
.get<ApiResponse<BackendPage<ApiPermissionResponseDto>>>('/v1/permissions', {
|
||||||
|
params: toBackendQuery(query),
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const page = normalizePage(unwrapApiResponse(response), query.page, query.pageSize)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...page,
|
||||||
|
records: page.records.map(mapPermission),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = query.keyword?.trim().toLowerCase()
|
||||||
|
const filteredRecords = mockRecords.filter((record) => {
|
||||||
|
return (
|
||||||
|
!keyword ||
|
||||||
|
[record.code, record.name, record.resourceType, record.resourcePath]
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(keyword)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const page = Math.max(query.page, 1)
|
||||||
|
const pageSize = Math.max(query.pageSize, 1)
|
||||||
|
const start = (page - 1) * pageSize
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
records: filteredRecords.slice(start, start + pageSize).map((record) => ({ ...record })),
|
||||||
|
total: filteredRecords.length,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPermission(id: string): Promise<ApiPermissionRecord | null> {
|
||||||
|
if (useMockData) {
|
||||||
|
const record = mockRecords.find((item) => item.id === id)
|
||||||
|
return record ? { ...record } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request.get<ApiResponse<ApiPermissionResponseDto>>(
|
||||||
|
`/v1/permissions/${encodeURIComponent(id)}`,
|
||||||
|
)
|
||||||
|
return mapPermission(unwrapApiResponse(response))
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPermission(
|
||||||
|
payload: CreateApiPermissionRequest,
|
||||||
|
): Promise<ApiPermissionRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行 API 权限写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.post<ApiResponse<ApiPermissionResponseDto>>('/v1/permissions', payload)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then(mapPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePermission(
|
||||||
|
id: string,
|
||||||
|
payload: UpdateApiPermissionRequest,
|
||||||
|
): Promise<ApiPermissionRecord> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行 API 权限写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.put<ApiResponse<ApiPermissionResponseDto>>(
|
||||||
|
`/v1/permissions/${encodeURIComponent(id)}`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
.then(unwrapApiResponse)
|
||||||
|
.then(mapPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePermission(id: string): Promise<void> {
|
||||||
|
if (useMockData) {
|
||||||
|
return Promise.reject(new Error('Mock 模式不执行 API 权限写操作'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
.delete<ApiResponse<null>>(`/v1/permissions/${encodeURIComponent(id)}`)
|
||||||
|
.then((response) => {
|
||||||
|
unwrapNullableApiResponse(response)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -146,6 +146,60 @@ export interface CreateRoleRequest {
|
|||||||
|
|
||||||
export type UpdateRoleRequest = Partial<CreateRoleRequest>
|
export type UpdateRoleRequest = Partial<CreateRoleRequest>
|
||||||
|
|
||||||
|
export type ApiPermissionStatus = 'ENABLED' | 'DISABLED'
|
||||||
|
export type ApiPermissionAction = 'QUERY' | 'CREATE' | 'UPDATE' | 'DISABLE'
|
||||||
|
|
||||||
|
export interface ApiPermissionQuery extends PageQuery {
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiPermissionRecord {
|
||||||
|
id: string
|
||||||
|
parentId: string | null
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
resourceType: string
|
||||||
|
resourcePath: string
|
||||||
|
action: ApiPermissionAction | string
|
||||||
|
status: ApiPermissionStatus
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiPermissionResponseDto {
|
||||||
|
id: string
|
||||||
|
parentId?: string | null
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
resourceType: string
|
||||||
|
resourcePath?: string | null
|
||||||
|
action?: ApiPermissionAction | string | null
|
||||||
|
status?: ApiPermissionStatus | null
|
||||||
|
createdAt?: string | null
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateApiPermissionRequest {
|
||||||
|
parentId?: string | null
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
resourceType: string
|
||||||
|
resourcePath?: string
|
||||||
|
action?: ApiPermissionAction
|
||||||
|
status?: ApiPermissionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateApiPermissionRequest {
|
||||||
|
parentId?: string | null
|
||||||
|
name: string
|
||||||
|
resourceType: string
|
||||||
|
resourcePath?: string
|
||||||
|
action?: ApiPermissionAction
|
||||||
|
status: ApiPermissionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiPermissionListResponse = PageResult<ApiPermissionRecord>
|
||||||
|
|
||||||
export interface CampusRecord {
|
export interface CampusRecord {
|
||||||
id: string
|
id: string
|
||||||
code: string
|
code: string
|
||||||
|
|||||||
@@ -46,21 +46,23 @@ function getCollectionRecords(collection: ArtifactCollection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeArtifact(dto: SignArtifactResponseDto): SigningArtifact {
|
function normalizeArtifact(dto: SignArtifactResponseDto): SigningArtifact {
|
||||||
const artifactType: SignArtifactType = dto.artifactType ?? dto.type ?? 'UNKNOWN'
|
const artifactType: SignArtifactType = dto.artifactType
|
||||||
const label = artifactLabels[artifactType] ?? '签署文件'
|
const label = artifactLabels[artifactType] ?? '签署文件'
|
||||||
const mimeType = dto.mimeType ?? dto.contentType ?? 'application/octet-stream'
|
const mimeType = dto.mimeType || 'application/octet-stream'
|
||||||
const extension =
|
const extension =
|
||||||
mimeType === 'application/pdf' ? 'pdf' : mimeType === 'image/png' ? 'png' : 'bin'
|
mimeType === 'application/pdf' ? 'pdf' : mimeType === 'image/png' ? 'png' : 'bin'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
taskId: dto.taskId,
|
taskId: dto.taskId,
|
||||||
|
pipelineId: dto.pipelineId,
|
||||||
artifactType,
|
artifactType,
|
||||||
label,
|
label,
|
||||||
fileName: dto.fileName || `${label}.${extension}`,
|
fileName: `${label}.${extension}`,
|
||||||
mimeType,
|
mimeType,
|
||||||
size: dto.size ?? dto.byteSize ?? dto.fileSize ?? 0,
|
sizeBytes: dto.sizeBytes,
|
||||||
sha256: dto.sha256 ?? dto.contentSha256 ?? '',
|
sha256: dto.sha256,
|
||||||
|
metadata: dto.metadata ?? {},
|
||||||
createdAt: formatDateTime(dto.createdAt),
|
createdAt: formatDateTime(dto.createdAt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,15 +41,12 @@ export function sendSigningSms(
|
|||||||
|
|
||||||
export function resendSigningSms(
|
export function resendSigningSms(
|
||||||
taskId: string,
|
taskId: string,
|
||||||
expectedRowVersion?: number,
|
|
||||||
idempotencyKey?: string,
|
idempotencyKey?: string,
|
||||||
): Promise<SignDeliveryResponseDto | null> {
|
): Promise<SignDeliveryResponseDto | null> {
|
||||||
const data = expectedRowVersion === undefined ? undefined : { expectedRowVersion }
|
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post<ApiResponseOf<SignDeliveryResponseDto | null>>(
|
.post<ApiResponseOf<SignDeliveryResponseDto | null>>(
|
||||||
`/v1/sign-deliveries/${encodeURIComponent(taskId)}/sms/resend`,
|
`/v1/sign-deliveries/${encodeURIComponent(taskId)}/sms/resend`,
|
||||||
data,
|
undefined,
|
||||||
{ headers: idempotencyHeaders(idempotencyKey) },
|
{ headers: idempotencyHeaders(idempotencyKey) },
|
||||||
)
|
)
|
||||||
.then(unwrapNullableApiResponse)
|
.then(unwrapNullableApiResponse)
|
||||||
@@ -69,9 +66,14 @@ export function createPadSigningSession(
|
|||||||
.then(unwrapApiResponse)
|
.then(unwrapApiResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function consumeSigningToken(payload: TokenConsumeRequest): Promise<TokenConsumeResponse> {
|
export function consumeSigningToken(
|
||||||
|
payload: TokenConsumeRequest,
|
||||||
|
idempotencyKey?: string,
|
||||||
|
): Promise<TokenConsumeResponse> {
|
||||||
return request
|
return request
|
||||||
.post<ApiResponseOf<TokenConsumeResponse>>('/v1/sign-deliveries/token/consume', payload)
|
.post<ApiResponseOf<TokenConsumeResponse>>('/v1/sign-deliveries/token/consume', payload, {
|
||||||
|
headers: idempotencyHeaders(idempotencyKey),
|
||||||
|
})
|
||||||
.then(unwrapApiResponse)
|
.then(unwrapApiResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
AvailableTemplateVersionResponseDto,
|
AvailableTemplateVersionResponseDto,
|
||||||
BackendSigningMethod,
|
BackendSigningMethod,
|
||||||
BackendSigningTaskStatus,
|
BackendSigningTaskStatus,
|
||||||
|
BackendVisitType,
|
||||||
CreateSigningTaskInput,
|
CreateSigningTaskInput,
|
||||||
CreateSigningTaskRequest,
|
CreateSigningTaskRequest,
|
||||||
PatientProfile,
|
PatientProfile,
|
||||||
@@ -563,7 +564,6 @@ function toBackendTaskQuery(query: SigningTaskQuery, options: SigningApiOptions
|
|||||||
pending: 'WAITING_SIGN',
|
pending: 'WAITING_SIGN',
|
||||||
signing: 'GENERATING',
|
signing: 'GENERATING',
|
||||||
signed: 'SIGNED',
|
signed: 'SIGNED',
|
||||||
rejected: 'FAILED',
|
|
||||||
expired: 'EXPIRED',
|
expired: 'EXPIRED',
|
||||||
void: 'VOIDED',
|
void: 'VOIDED',
|
||||||
failed: 'FAILED',
|
failed: 'FAILED',
|
||||||
@@ -579,6 +579,19 @@ function toBackendTaskQuery(query: SigningTaskQuery, options: SigningApiOptions
|
|||||||
params.signMethod = query.method === 'sms' ? 'SMS' : 'PAD'
|
params.signMethod = query.method === 'sms' ? 'SMS' : 'PAD'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.visitType && query.visitType !== 'all') {
|
||||||
|
const visitTypeMap: Partial<Record<VisitType, BackendVisitType>> = {
|
||||||
|
门诊: 'OUTPATIENT',
|
||||||
|
住院: 'INPATIENT',
|
||||||
|
体检: 'CHECKUP',
|
||||||
|
}
|
||||||
|
const visitType = visitTypeMap[query.visitType]
|
||||||
|
|
||||||
|
if (visitType) {
|
||||||
|
params.visitType = visitType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (query.documentId) {
|
if (query.documentId) {
|
||||||
params.templateVersionId =
|
params.templateVersionId =
|
||||||
findTemplate(options.templates, query.documentId)?.versionId ?? query.documentId
|
findTemplate(options.templates, query.documentId)?.versionId ?? query.documentId
|
||||||
@@ -604,8 +617,12 @@ function toBackendTaskQuery(query: SigningTaskQuery, options: SigningApiOptions
|
|||||||
params.createdTo = end.toISOString()
|
params.createdTo = end.toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.campus && query.campus !== 'all' && options.campusId) {
|
if (query.campus && query.campus !== 'all') {
|
||||||
params.campusId = options.campusId
|
const campusId = options.campusIds?.[query.campus] ?? options.campusId
|
||||||
|
|
||||||
|
if (campusId) {
|
||||||
|
params.campusId = campusId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.department && options.departmentIds?.[query.department]) {
|
if (query.department && options.departmentIds?.[query.department]) {
|
||||||
@@ -1031,7 +1048,7 @@ export async function resendSigningSms(
|
|||||||
options: SigningTaskActionOptions = {},
|
options: SigningTaskActionOptions = {},
|
||||||
): Promise<SigningTaskRecord | null> {
|
): Promise<SigningTaskRecord | null> {
|
||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
await resendSigningSmsDelivery(id, options.expectedRowVersion)
|
await resendSigningSmsDelivery(id)
|
||||||
return getSigningTaskDetail(id, options)
|
return getSigningTaskDetail(id, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1107,7 +1124,6 @@ export function getSigningStatusLabel(status: SigningTaskStatus) {
|
|||||||
pending: '待签署',
|
pending: '待签署',
|
||||||
signing: '签署中',
|
signing: '签署中',
|
||||||
signed: '已签署',
|
signed: '已签署',
|
||||||
rejected: '已拒签',
|
|
||||||
expired: '已超时',
|
expired: '已超时',
|
||||||
void: '已作废',
|
void: '已作废',
|
||||||
failed: '处理失败',
|
failed: '处理失败',
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ export interface WorkbenchOverviewQuery {
|
|||||||
rankingPeriod: HomeRankingPeriod
|
rankingPeriod: HomeRankingPeriod
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SigningTaskStatus =
|
export type SigningTaskStatus = 'pending' | 'signing' | 'signed' | 'expired' | 'void' | 'failed'
|
||||||
'pending' | 'signing' | 'signed' | 'rejected' | 'expired' | 'void' | 'failed'
|
|
||||||
|
|
||||||
export type PatientSex = '男' | '女' | '未知'
|
export type PatientSex = '男' | '女' | '未知'
|
||||||
|
|
||||||
@@ -171,6 +170,7 @@ export interface SigningApiOptions {
|
|||||||
templates?: SigningTemplate[]
|
templates?: SigningTemplate[]
|
||||||
campus?: WorkbenchCampus
|
campus?: WorkbenchCampus
|
||||||
campusId?: string
|
campusId?: string
|
||||||
|
campusIds?: Record<string, string>
|
||||||
campusNames?: Record<string, WorkbenchCampus>
|
campusNames?: Record<string, WorkbenchCampus>
|
||||||
departmentIds?: Record<string, string>
|
departmentIds?: Record<string, string>
|
||||||
}
|
}
|
||||||
@@ -438,32 +438,29 @@ export interface SignTaskEventResponseDto {
|
|||||||
|
|
||||||
export type SignArtifactType = 'ORIGINAL_PDF' | 'SIGNATURE_IMAGE' | 'SIGNED_PDF' | string
|
export type SignArtifactType = 'ORIGINAL_PDF' | 'SIGNATURE_IMAGE' | 'SIGNED_PDF' | string
|
||||||
|
|
||||||
/** MEDISIGN 签署产物 DTO。部分文件元数据由不同版本的服务端返回,字段保持可选并在 API 边界归一化。 */
|
|
||||||
export interface SignArtifactResponseDto {
|
export interface SignArtifactResponseDto {
|
||||||
id: string
|
id: string
|
||||||
taskId: string
|
taskId: string
|
||||||
artifactType?: SignArtifactType
|
pipelineId: string
|
||||||
type?: SignArtifactType
|
artifactType: SignArtifactType
|
||||||
fileName?: string | null
|
sizeBytes: number
|
||||||
mimeType?: string | null
|
mimeType: string
|
||||||
contentType?: string | null
|
sha256: string
|
||||||
size?: number | null
|
metadata: Record<string, unknown>
|
||||||
byteSize?: number | null
|
createdAt: string
|
||||||
fileSize?: number | null
|
|
||||||
sha256?: string | null
|
|
||||||
contentSha256?: string | null
|
|
||||||
createdAt?: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SigningArtifact {
|
export interface SigningArtifact {
|
||||||
id: string
|
id: string
|
||||||
taskId: string
|
taskId: string
|
||||||
|
pipelineId: string
|
||||||
artifactType: SignArtifactType
|
artifactType: SignArtifactType
|
||||||
label: string
|
label: string
|
||||||
fileName: string
|
fileName: string
|
||||||
mimeType: string
|
mimeType: string
|
||||||
size: number
|
sizeBytes: number
|
||||||
sha256: string
|
sha256: string
|
||||||
|
metadata: Record<string, unknown>
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export type AuthUserStatus = 'ENABLED' | 'DISABLED'
|
|||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
|
captchaId?: string
|
||||||
|
captchaCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CaptchaResponse {
|
||||||
|
captchaId: string
|
||||||
|
imageBase64: string
|
||||||
|
expiresAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticatedUser {
|
export interface AuthenticatedUser {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { getAuthErrorMessage } from '@/api/auth'
|
import { getAuthErrorMessage, getCaptcha } from '@/api/auth'
|
||||||
|
import type { CaptchaResponse, LoginRequest } from '@/types/auth'
|
||||||
import { useLoginStore } from '@/stores/login'
|
import { useLoginStore } from '@/stores/login'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -11,9 +12,27 @@ const loginStore = useLoginStore()
|
|||||||
|
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
|
const captcha = ref<CaptchaResponse | null>(null)
|
||||||
|
const captchaCode = ref('')
|
||||||
|
const captchaLoading = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const isSubmitting = ref(false)
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
async function loadCaptcha() {
|
||||||
|
captchaLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
captcha.value = await getCaptcha()
|
||||||
|
captchaCode.value = ''
|
||||||
|
} catch {
|
||||||
|
// 验证码由服务端按需启用;接口不可用时保持账号密码登录兼容。
|
||||||
|
captcha.value = null
|
||||||
|
captchaCode.value = ''
|
||||||
|
} finally {
|
||||||
|
captchaLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getRedirectPath() {
|
function getRedirectPath() {
|
||||||
const redirect = route.query.redirect
|
const redirect = route.query.redirect
|
||||||
return typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')
|
return typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')
|
||||||
@@ -29,22 +48,43 @@ async function handleLogin() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (captcha.value && !captchaCode.value.trim()) {
|
||||||
|
errorMessage.value = '请输入图形验证码'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loginStore.login({
|
const payload: LoginRequest = {
|
||||||
username: normalizedUsername,
|
username: normalizedUsername,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
})
|
...(captcha.value
|
||||||
|
? {
|
||||||
|
captchaId: captcha.value.captchaId,
|
||||||
|
captchaCode: captchaCode.value.trim(),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
await loginStore.login(payload)
|
||||||
|
|
||||||
await router.replace(getRedirectPath())
|
await router.replace(getRedirectPath())
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
errorMessage.value = getAuthErrorMessage(error)
|
errorMessage.value = getAuthErrorMessage(error)
|
||||||
|
|
||||||
|
if (captcha.value) {
|
||||||
|
void loadCaptcha()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false
|
isSubmitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadCaptcha()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -98,6 +138,30 @@ async function handleLogin() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="captcha" class="login-field login-captcha">
|
||||||
|
<label for="login-captcha">验证码</label>
|
||||||
|
<div class="captcha-row">
|
||||||
|
<input
|
||||||
|
id="login-captcha"
|
||||||
|
v-model="captchaCode"
|
||||||
|
type="text"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
inputmode="text"
|
||||||
|
maxlength="8"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="captcha-image-button"
|
||||||
|
:disabled="captchaLoading"
|
||||||
|
aria-label="刷新验证码"
|
||||||
|
@click="loadCaptcha"
|
||||||
|
>
|
||||||
|
<img :src="captcha.imageBase64" alt="图形验证码,点击刷新" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p v-if="errorMessage" class="login-error" role="alert">{{ errorMessage }}</p>
|
<p v-if="errorMessage" class="login-error" role="alert">{{ errorMessage }}</p>
|
||||||
|
|
||||||
<button class="login-submit" type="submit" :disabled="isSubmitting">
|
<button class="login-submit" type="submit" :disabled="isSubmitting">
|
||||||
@@ -192,6 +256,45 @@ async function handleLogin() {
|
|||||||
border-color: var(--brand);
|
border-color: var(--brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captcha-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-row input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image-button {
|
||||||
|
display: grid;
|
||||||
|
width: 112px;
|
||||||
|
height: 34px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f4f7f9;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image-button:hover:not(:disabled) {
|
||||||
|
border-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image-button:disabled {
|
||||||
|
cursor: wait;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-image-button img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.login-error {
|
.login-error {
|
||||||
margin: -4px 0 10px;
|
margin: -4px 0 10px;
|
||||||
color: var(--err);
|
color: var(--err);
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ function mapReportTask(task: SigningTaskRecord, templates: SigningTemplate[]): R
|
|||||||
? 'signed'
|
? 'signed'
|
||||||
: task.status === 'expired'
|
: task.status === 'expired'
|
||||||
? 'expired'
|
? 'expired'
|
||||||
: task.status === 'void' || task.status === 'failed' || task.status === 'rejected'
|
: task.status === 'void' || task.status === 'failed'
|
||||||
? 'void'
|
? 'void'
|
||||||
: 'pending'
|
: 'pending'
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function formatFileSize(size: number) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="artifact-info">
|
<div class="artifact-info">
|
||||||
<strong>{{ artifact.label }}</strong>
|
<strong>{{ artifact.label }}</strong>
|
||||||
<span>{{ artifact.fileName }} · {{ formatFileSize(artifact.size) }}</span>
|
<span>{{ artifact.fileName }} · {{ formatFileSize(artifact.sizeBytes) }}</span>
|
||||||
<small v-if="artifact.createdAt">生成于 {{ artifact.createdAt }}</small>
|
<small v-if="artifact.createdAt">生成于 {{ artifact.createdAt }}</small>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="artifact-download" @click="emit('download', artifact)">
|
<button type="button" class="artifact-download" @click="emit('download', artifact)">
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const emit = defineEmits<{
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="task.status === 'expired' || task.status === 'failed'">
|
<template v-else-if="task.status === 'expired'">
|
||||||
<button type="button" class="action-button" @click="emit('action', 'reopen')">
|
<button type="button" class="action-button" @click="emit('action', 'reopen')">
|
||||||
↻ 重新发起
|
↻ 重新发起
|
||||||
</button>
|
</button>
|
||||||
@@ -105,7 +105,7 @@ const emit = defineEmits<{
|
|||||||
class="action-button action-button--ghost"
|
class="action-button action-button--ghost"
|
||||||
@click="emit('action', 'download-pdf')"
|
@click="emit('action', 'download-pdf')"
|
||||||
>
|
>
|
||||||
下载 PDF 原件
|
下载签署后 PDF
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import type { SigningTaskRecord } from '@/api/workbench/types'
|
import type { SigningTaskRecord } from '@/api/workbench/types'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
tasks: SigningTaskRecord[]
|
tasks: SigningTaskRecord[]
|
||||||
selectedId: string | null
|
selectedId: string | null
|
||||||
total: number
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
loading: boolean
|
loading: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
select: [id: string]
|
select: [id: string]
|
||||||
|
pageChange: [page: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const pageCount = computed(() => Math.max(Math.ceil(props.total / props.pageSize), 1))
|
||||||
|
|
||||||
const statusLabels: Record<SigningTaskRecord['status'], string> = {
|
const statusLabels: Record<SigningTaskRecord['status'], string> = {
|
||||||
pending: '待签署',
|
pending: '待签署',
|
||||||
signing: '签署中',
|
signing: '签署中',
|
||||||
signed: '已签署',
|
signed: '已签署',
|
||||||
rejected: '已拒签',
|
|
||||||
expired: '已超时',
|
expired: '已超时',
|
||||||
void: '已作废',
|
void: '已作废',
|
||||||
failed: '处理失败',
|
failed: '处理失败',
|
||||||
@@ -100,6 +106,21 @@ function getVisitClass(type: SigningTaskRecord['visitType']) {
|
|||||||
<span aria-hidden="true">🔍</span>
|
<span aria-hidden="true">🔍</span>
|
||||||
<p>没有符合条件的任务</p>
|
<p>没有符合条件的任务</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer v-if="total > pageSize" class="task-pagination">
|
||||||
|
<span>第 {{ page }} / {{ pageCount }} 页</span>
|
||||||
|
<span class="task-pagination__total">共 {{ total }} 条</span>
|
||||||
|
<button type="button" :disabled="page <= 1 || loading" @click="emit('pageChange', page - 1)">
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:disabled="page >= pageCount || loading"
|
||||||
|
@click="emit('pageChange', page + 1)"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -212,7 +233,6 @@ function getVisitClass(type: SigningTaskRecord['visitType']) {
|
|||||||
background: var(--ok-l);
|
background: var(--ok-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-status--rejected,
|
|
||||||
.task-status--expired {
|
.task-status--expired {
|
||||||
color: var(--err);
|
color: var(--err);
|
||||||
background: var(--err-l);
|
background: var(--err-l);
|
||||||
@@ -292,6 +312,42 @@ function getVisitClass(type: SigningTaskRecord['visitType']) {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-pagination {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 9px 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--mut);
|
||||||
|
font-size: 11.5px;
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: var(--r);
|
||||||
|
box-shadow: var(--sh);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-pagination__total {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-pagination button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: var(--brand);
|
||||||
|
font-size: 11.5px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-pagination button:hover:not(:disabled) {
|
||||||
|
border-color: var(--brand);
|
||||||
|
background: var(--brand-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-pagination button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
.task-items--loading {
|
.task-items--loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const STATUS_VALUES: SigningTaskStatus[] = [
|
|||||||
'pending',
|
'pending',
|
||||||
'signing',
|
'signing',
|
||||||
'signed',
|
'signed',
|
||||||
'rejected',
|
|
||||||
'expired',
|
'expired',
|
||||||
'void',
|
'void',
|
||||||
'failed',
|
'failed',
|
||||||
@@ -59,7 +58,6 @@ const statusLabels: Record<SigningTaskStatus, string> = {
|
|||||||
pending: '待签署',
|
pending: '待签署',
|
||||||
signing: '签署中',
|
signing: '签署中',
|
||||||
signed: '已签署',
|
signed: '已签署',
|
||||||
rejected: '已拒签',
|
|
||||||
expired: '已超时',
|
expired: '已超时',
|
||||||
void: '已作废',
|
void: '已作废',
|
||||||
failed: '处理失败',
|
failed: '处理失败',
|
||||||
@@ -78,6 +76,8 @@ const filter = reactive<SigningFilterForm>({
|
|||||||
|
|
||||||
const tasks = ref<SigningTaskRecord[]>([])
|
const tasks = ref<SigningTaskRecord[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
const selectedTaskId = ref<string | null>(null)
|
const selectedTaskId = ref<string | null>(null)
|
||||||
const selectedTask = ref<SigningTaskRecord | null>(null)
|
const selectedTask = ref<SigningTaskRecord | null>(null)
|
||||||
const taskEvents = ref<SigningTaskEvent[]>([])
|
const taskEvents = ref<SigningTaskEvent[]>([])
|
||||||
@@ -145,6 +145,7 @@ const documentOptions = computed<SigningFilterOption[]>(() => [
|
|||||||
const signingApiOptions = computed(() => ({
|
const signingApiOptions = computed(() => ({
|
||||||
campus: appStore.selectedCampus,
|
campus: appStore.selectedCampus,
|
||||||
campusId: campusIds.value[appStore.selectedCampus],
|
campusId: campusIds.value[appStore.selectedCampus],
|
||||||
|
campusIds: campusIds.value,
|
||||||
campusNames: campusNames.value,
|
campusNames: campusNames.value,
|
||||||
departmentIds: departmentIds.value,
|
departmentIds: departmentIds.value,
|
||||||
templates: templates.value,
|
templates: templates.value,
|
||||||
@@ -275,7 +276,11 @@ async function loadTaskDetail(id: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTasks(preferredTaskId?: string) {
|
async function loadTasks(preferredTaskId?: string, resetPage = false) {
|
||||||
|
if (resetPage) {
|
||||||
|
page.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
const requestId = ++listRequestId
|
const requestId = ++listRequestId
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
@@ -283,8 +288,8 @@ async function loadTasks(preferredTaskId?: string) {
|
|||||||
const response = await getSigningTasks(
|
const response = await getSigningTasks(
|
||||||
{
|
{
|
||||||
...filter,
|
...filter,
|
||||||
page: 1,
|
page: page.value,
|
||||||
pageSize: 50,
|
pageSize: pageSize.value,
|
||||||
},
|
},
|
||||||
signingApiOptions.value,
|
signingApiOptions.value,
|
||||||
)
|
)
|
||||||
@@ -295,6 +300,8 @@ async function loadTasks(preferredTaskId?: string) {
|
|||||||
|
|
||||||
tasks.value = response.records
|
tasks.value = response.records
|
||||||
total.value = response.total
|
total.value = response.total
|
||||||
|
page.value = response.page
|
||||||
|
pageSize.value = response.pageSize
|
||||||
|
|
||||||
const taskIds = new Set(response.records.map((task) => task.id))
|
const taskIds = new Set(response.records.map((task) => task.id))
|
||||||
const routeTaskId = getQueryString(route.query.taskId)
|
const routeTaskId = getQueryString(route.query.taskId)
|
||||||
@@ -323,7 +330,7 @@ async function loadTasks(preferredTaskId?: string) {
|
|||||||
function handleSearch(nextFilter: SigningFilterForm) {
|
function handleSearch(nextFilter: SigningFilterForm) {
|
||||||
Object.assign(filter, nextFilter)
|
Object.assign(filter, nextFilter)
|
||||||
selectedTaskId.value = null
|
selectedTaskId.value = null
|
||||||
void loadTasks()
|
void loadTasks(undefined, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTask(id: string) {
|
function selectTask(id: string) {
|
||||||
@@ -331,13 +338,25 @@ function selectTask(id: string) {
|
|||||||
void loadTaskDetail(id)
|
void loadTaskDetail(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePageChange(nextPage: number) {
|
||||||
|
const pageCount = Math.max(Math.ceil(total.value / pageSize.value), 1)
|
||||||
|
|
||||||
|
if (nextPage < 1 || nextPage > pageCount || nextPage === page.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page.value = nextPage
|
||||||
|
selectedTaskId.value = null
|
||||||
|
void loadTasks()
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshTask(updatedTask: SigningTaskRecord | null, successMessage: string) {
|
async function refreshTask(updatedTask: SigningTaskRecord | null, successMessage: string) {
|
||||||
if (!updatedTask) {
|
if (!updatedTask) {
|
||||||
ElMessage.error('签署任务不存在或已被删除')
|
ElMessage.error('签署任务不存在或已被删除')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadTasks(updatedTask.id)
|
await loadTasks(updatedTask.id, true)
|
||||||
ElMessage.success(successMessage)
|
ElMessage.success(successMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,15 +408,18 @@ async function handleTaskAction(action: SigningTaskAction) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === 'reopen' && task.status !== 'expired') {
|
||||||
|
ElMessage.info('只有已超时任务可以重新发起')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'download-pdf') {
|
if (action === 'download-pdf') {
|
||||||
const artifact =
|
const artifact = artifacts.value.find((item) => item.artifactType === 'SIGNED_PDF')
|
||||||
artifacts.value.find((item) => item.artifactType === 'SIGNED_PDF') ??
|
|
||||||
artifacts.value.find((item) => item.artifactType === 'ORIGINAL_PDF')
|
|
||||||
|
|
||||||
if (artifact) {
|
if (artifact) {
|
||||||
await downloadArtifact(artifact)
|
await downloadArtifact(artifact)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.info('当前任务暂无 PDF 文件')
|
ElMessage.info('当前任务暂无签署后 PDF')
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -535,13 +557,14 @@ async function initializePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleTaskCreated(task: SigningTaskRecord) {
|
async function handleTaskCreated(task: SigningTaskRecord) {
|
||||||
await loadTasks(task.id)
|
await loadTasks(task.id, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [route.query.status, route.query.range, route.query.taskId],
|
() => [route.query.status, route.query.range, route.query.taskId],
|
||||||
() => {
|
() => {
|
||||||
syncFilterFromRoute()
|
syncFilterFromRoute()
|
||||||
|
page.value = 1
|
||||||
selectedTaskId.value = null
|
selectedTaskId.value = null
|
||||||
void loadTasks()
|
void loadTasks()
|
||||||
},
|
},
|
||||||
@@ -553,6 +576,7 @@ watch(
|
|||||||
if (filter.campus !== 'all') {
|
if (filter.campus !== 'all') {
|
||||||
filter.campus = campus
|
filter.campus = campus
|
||||||
}
|
}
|
||||||
|
page.value = 1
|
||||||
void loadTasks()
|
void loadTasks()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -593,8 +617,11 @@ onMounted(() => {
|
|||||||
:tasks="tasks"
|
:tasks="tasks"
|
||||||
:selected-id="selectedTaskId"
|
:selected-id="selectedTaskId"
|
||||||
:total="total"
|
:total="total"
|
||||||
|
:page="page"
|
||||||
|
:page-size="pageSize"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@select="selectTask"
|
@select="selectTask"
|
||||||
|
@page-change="handlePageChange"
|
||||||
/>
|
/>
|
||||||
<SigningTaskDetail
|
<SigningTaskDetail
|
||||||
:task="selectedTask"
|
:task="selectedTask"
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import type {
|
|||||||
|
|
||||||
export function consumeSigningToken(payload: TokenConsumeRequest): Promise<TokenConsumeResponse> {
|
export function consumeSigningToken(payload: TokenConsumeRequest): Promise<TokenConsumeResponse> {
|
||||||
return request
|
return request
|
||||||
.post<ApiResponse<TokenConsumeResponse>>('/v1/sign-deliveries/token/consume', payload)
|
.post<ApiResponse<TokenConsumeResponse>>('/v1/sign-deliveries/token/consume', payload, {
|
||||||
|
headers: {
|
||||||
|
'Idempotency-Key': createIdempotencyKey(),
|
||||||
|
},
|
||||||
|
})
|
||||||
.then(unwrapApiResponse)
|
.then(unwrapApiResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user